Skip to content

Commit ae45fb5

Browse files
fix: Prevent backslashes in descriptions from breaking docstrings [#735]. Thanks @robertschweizer & @bryan-hunt! (#735)
Co-authored-by: Dylan Anthony <[email protected]>
1 parent 555ff10 commit ae45fb5

File tree

9 files changed

+200
-8
lines changed

9 files changed

+200
-8
lines changed

end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from . import (
66
callback_test,
77
defaults_tests_defaults_post,
8+
description_with_backslash,
89
get_basic_list_of_booleans,
910
get_basic_list_of_floats,
1011
get_basic_list_of_integers,
@@ -158,3 +159,10 @@ def callback_test(cls) -> types.ModuleType:
158159
Try sending a request related to a callback
159160
"""
160161
return callback_test
162+
163+
@classmethod
164+
def description_with_backslash(cls) -> types.ModuleType:
165+
"""
166+
Test description with \
167+
"""
168+
return description_with_backslash
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from http import HTTPStatus
2+
from typing import Any, Dict, Optional
3+
4+
import httpx
5+
6+
from ... import errors
7+
from ...client import Client
8+
from ...types import Response
9+
10+
11+
def _get_kwargs(
12+
*,
13+
client: Client,
14+
) -> Dict[str, Any]:
15+
url = "{}/tests/description-with-backslash".format(client.base_url)
16+
17+
headers: Dict[str, str] = client.get_headers()
18+
cookies: Dict[str, Any] = client.get_cookies()
19+
20+
return {
21+
"method": "get",
22+
"url": url,
23+
"headers": headers,
24+
"cookies": cookies,
25+
"timeout": client.get_timeout(),
26+
}
27+
28+
29+
def _parse_response(*, client: Client, response: httpx.Response) -> Optional[Any]:
30+
if response.status_code == HTTPStatus.OK:
31+
return None
32+
if client.raise_on_unexpected_status:
33+
raise errors.UnexpectedStatus(f"Unexpected status code: {response.status_code}")
34+
else:
35+
return None
36+
37+
38+
def _build_response(*, client: Client, response: httpx.Response) -> Response[Any]:
39+
return Response(
40+
status_code=HTTPStatus(response.status_code),
41+
content=response.content,
42+
headers=response.headers,
43+
parsed=_parse_response(client=client, response=response),
44+
)
45+
46+
47+
def sync_detailed(
48+
*,
49+
client: Client,
50+
) -> Response[Any]:
51+
r""" Test description with \
52+
53+
Test description with \
54+
55+
Raises:
56+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
57+
httpx.TimeoutException: If the request takes longer than Client.timeout.
58+
59+
Returns:
60+
Response[Any]
61+
"""
62+
63+
kwargs = _get_kwargs(
64+
client=client,
65+
)
66+
67+
response = httpx.request(
68+
verify=client.verify_ssl,
69+
**kwargs,
70+
)
71+
72+
return _build_response(client=client, response=response)
73+
74+
75+
async def asyncio_detailed(
76+
*,
77+
client: Client,
78+
) -> Response[Any]:
79+
r""" Test description with \
80+
81+
Test description with \
82+
83+
Raises:
84+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
85+
httpx.TimeoutException: If the request takes longer than Client.timeout.
86+
87+
Returns:
88+
Response[Any]
89+
"""
90+
91+
kwargs = _get_kwargs(
92+
client=client,
93+
)
94+
95+
async with httpx.AsyncClient(verify=client.verify_ssl) as _client:
96+
response = await _client.request(**kwargs)
97+
98+
return _build_response(client=client, response=response)

end_to_end_tests/golden-record/my_test_api_client/models/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from .model_with_additional_properties_refed import ModelWithAdditionalPropertiesRefed
4848
from .model_with_any_json_properties import ModelWithAnyJsonProperties
4949
from .model_with_any_json_properties_additional_property_type_0 import ModelWithAnyJsonPropertiesAdditionalPropertyType0
50+
from .model_with_backslash_in_description import ModelWithBackslashInDescription
5051
from .model_with_circular_ref_a import ModelWithCircularRefA
5152
from .model_with_circular_ref_b import ModelWithCircularRefB
5253
from .model_with_circular_ref_in_additional_properties_a import ModelWithCircularRefInAdditionalPropertiesA
@@ -111,6 +112,7 @@
111112
"ModelWithAdditionalPropertiesRefed",
112113
"ModelWithAnyJsonProperties",
113114
"ModelWithAnyJsonPropertiesAdditionalPropertyType0",
115+
"ModelWithBackslashInDescription",
114116
"ModelWithCircularRefA",
115117
"ModelWithCircularRefB",
116118
"ModelWithCircularRefInAdditionalPropertiesA",
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from typing import Any, Dict, List, Type, TypeVar
2+
3+
import attr
4+
5+
T = TypeVar("T", bound="ModelWithBackslashInDescription")
6+
7+
8+
@attr.s(auto_attribs=True)
9+
class ModelWithBackslashInDescription:
10+
r""" Description with special character: \
11+
12+
"""
13+
14+
additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict)
15+
16+
def to_dict(self) -> Dict[str, Any]:
17+
18+
field_dict: Dict[str, Any] = {}
19+
field_dict.update(self.additional_properties)
20+
field_dict.update({})
21+
22+
return field_dict
23+
24+
@classmethod
25+
def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
26+
d = src_dict.copy()
27+
model_with_backslash_in_description = cls()
28+
29+
model_with_backslash_in_description.additional_properties = d
30+
return model_with_backslash_in_description
31+
32+
@property
33+
def additional_keys(self) -> List[str]:
34+
return list(self.additional_properties.keys())
35+
36+
def __getitem__(self, key: str) -> Any:
37+
return self.additional_properties[key]
38+
39+
def __setitem__(self, key: str, value: Any) -> None:
40+
self.additional_properties[key] = value
41+
42+
def __delitem__(self, key: str) -> None:
43+
del self.additional_properties[key]
44+
45+
def __contains__(self, key: str) -> bool:
46+
return key in self.additional_properties

end_to_end_tests/openapi.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,6 +1229,21 @@
12291229
}
12301230
}
12311231
}
1232+
},
1233+
"/tests/description-with-backslash": {
1234+
"get": {
1235+
"tags": [
1236+
"tests"
1237+
],
1238+
"summary": "Test description with \\",
1239+
"description": "Test description with \\",
1240+
"operationId": "description_with_backslash",
1241+
"responses": {
1242+
"200": {
1243+
"description": "Successful response"
1244+
}
1245+
}
1246+
}
12321247
}
12331248
},
12341249
"components": {
@@ -2223,6 +2238,10 @@
22232238
"$ref": "#/components/schemas/AnArrayWithACircularRefInItemsObjectAdditionalPropertiesA"
22242239
}
22252240
}
2241+
},
2242+
"ModelWithBackslashInDescription": {
2243+
"type": "object",
2244+
"description": "Description with special character: \\"
22262245
}
22272246
},
22282247
"parameters": {

openapi_python_client/templates/endpoint_macros.py.jinja

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{% from "property_templates/helpers.jinja" import guarded_statement %}
2+
{% from "helpers.jinja" import safe_docstring %}
23

34
{% macro header_params(endpoint) %}
45
{% if endpoint.header_parameters %}
@@ -140,8 +141,8 @@ json_body=json_body,
140141
{% endfor %}
141142
{% endmacro %}
142143

143-
{% macro docstring(endpoint, return_string) %}
144-
"""{% if endpoint.summary %}{{ endpoint.summary | wordwrap(100)}}
144+
{% macro docstring_content(endpoint, return_string) %}
145+
{% if endpoint.summary %}{{ endpoint.summary | wordwrap(100)}}
145146

146147
{% endif -%}
147148
{%- if endpoint.description %} {{ endpoint.description | wordwrap(100) }}
@@ -165,5 +166,8 @@ Raises:
165166

166167
Returns:
167168
Response[{{ return_string }}]
168-
"""
169+
{% endmacro %}
170+
171+
{% macro docstring(endpoint, return_string) %}
172+
{{ safe_docstring(docstring_content(endpoint, return_string)) }}
169173
{% endmacro %}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% macro safe_docstring(content) %}
2+
{# This macro returns the provided content as a docstring, set to a raw string if it contains a backslash #}
3+
{% if '\\' in content -%}
4+
r""" {{ content }} """
5+
{%- else -%}
6+
""" {{ content }} """
7+
{%- endif -%}
8+
{% endmacro %}

openapi_python_client/templates/model.py.jinja

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ if TYPE_CHECKING:
3131
{% set class_name = model.class_info.name %}
3232
{% set module_name = model.class_info.module_name %}
3333

34+
{% from "helpers.jinja" import safe_docstring %}
35+
3436
T = TypeVar("T", bound="{{ class_name }}")
3537

36-
@attr.s(auto_attribs=True)
37-
class {{ class_name }}:
38-
"""{% if model.title %}{{ model.title | wordwrap(116) }}
38+
{% macro class_docstring_content(model) %}
39+
{% if model.title %}{{ model.title | wordwrap(116) }}
3940

4041
{% endif -%}
4142
{%- if model.description %}{{ model.description | wordwrap(116) }}
@@ -55,7 +56,11 @@ class {{ class_name }}:
5556
{% for property in model.required_properties + model.optional_properties %}
5657
{{ property.to_docstring() | wordwrap(112) | indent(12) }}
5758
{% endfor %}{% endif %}
58-
"""
59+
{% endmacro %}
60+
61+
@attr.s(auto_attribs=True)
62+
class {{ class_name }}:
63+
{{ safe_docstring(class_docstring_content(model)) | indent(4) }}
5964

6065
{% for property in model.required_properties + model.optional_properties %}
6166
{% if property.default is none and property.required %}

openapi_python_client/templates/package_init.py.jinja

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
""" {{ package_description }} """
1+
{% from "helpers.jinja" import safe_docstring %}
2+
3+
{{ safe_docstring(package_description) }}
24
from .client import AuthenticatedClient, Client
35

46
__all__ = (

0 commit comments

Comments
 (0)