diff --git a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py index 13120943a..537c5c580 100644 --- a/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py +++ b/end_to_end_tests/custom-templates-golden-record/my_test_api_client/api/tests/__init__.py @@ -5,6 +5,7 @@ from . import ( callback_test, defaults_tests_defaults_post, + description_with_backslash, get_basic_list_of_booleans, get_basic_list_of_floats, get_basic_list_of_integers, @@ -158,3 +159,10 @@ def callback_test(cls) -> types.ModuleType: Try sending a request related to a callback """ return callback_test + + @classmethod + def description_with_backslash(cls) -> types.ModuleType: + """ + Test description with \ + """ + return description_with_backslash diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/description_with_backslash.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/description_with_backslash.py new file mode 100644 index 000000000..db605c979 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/description_with_backslash.py @@ -0,0 +1,98 @@ +from http import HTTPStatus +from typing import Any, Dict, Optional + +import httpx + +from ... import errors +from ...client import Client +from ...types import Response + + +def _get_kwargs( + *, + client: Client, +) -> Dict[str, Any]: + url = "{}/tests/description-with-backslash".format(client.base_url) + + headers: Dict[str, str] = client.get_headers() + cookies: Dict[str, Any] = client.get_cookies() + + return { + "method": "get", + "url": url, + "headers": headers, + "cookies": cookies, + "timeout": client.get_timeout(), + } + + +def _parse_response(*, client: Client, response: httpx.Response) -> Optional[Any]: + if response.status_code == HTTPStatus.OK: + return None + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(f"Unexpected status code: {response.status_code}") + else: + return None + + +def _build_response(*, client: Client, response: httpx.Response) -> Response[Any]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: Client, +) -> Response[Any]: + r""" Test description with \ + + Test description with \ + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + client=client, + ) + + response = httpx.request( + verify=client.verify_ssl, + **kwargs, + ) + + return _build_response(client=client, response=response) + + +async def asyncio_detailed( + *, + client: Client, +) -> Response[Any]: + r""" Test description with \ + + Test description with \ + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[Any] + """ + + kwargs = _get_kwargs( + client=client, + ) + + async with httpx.AsyncClient(verify=client.verify_ssl) as _client: + response = await _client.request(**kwargs) + + return _build_response(client=client, response=response) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py index c77cf0bc5..f63501d10 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/__init__.py @@ -47,6 +47,7 @@ from .model_with_additional_properties_refed import ModelWithAdditionalPropertiesRefed from .model_with_any_json_properties import ModelWithAnyJsonProperties from .model_with_any_json_properties_additional_property_type_0 import ModelWithAnyJsonPropertiesAdditionalPropertyType0 +from .model_with_backslash_in_description import ModelWithBackslashInDescription from .model_with_circular_ref_a import ModelWithCircularRefA from .model_with_circular_ref_b import ModelWithCircularRefB from .model_with_circular_ref_in_additional_properties_a import ModelWithCircularRefInAdditionalPropertiesA @@ -111,6 +112,7 @@ "ModelWithAdditionalPropertiesRefed", "ModelWithAnyJsonProperties", "ModelWithAnyJsonPropertiesAdditionalPropertyType0", + "ModelWithBackslashInDescription", "ModelWithCircularRefA", "ModelWithCircularRefB", "ModelWithCircularRefInAdditionalPropertiesA", diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/model_with_backslash_in_description.py b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_backslash_in_description.py new file mode 100644 index 000000000..d52952236 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/model_with_backslash_in_description.py @@ -0,0 +1,46 @@ +from typing import Any, Dict, List, Type, TypeVar + +import attr + +T = TypeVar("T", bound="ModelWithBackslashInDescription") + + +@attr.s(auto_attribs=True) +class ModelWithBackslashInDescription: + r""" Description with special character: \ + + """ + + additional_properties: Dict[str, Any] = attr.ib(init=False, factory=dict) + + def to_dict(self) -> Dict[str, Any]: + + field_dict: Dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({}) + + return field_dict + + @classmethod + def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T: + d = src_dict.copy() + model_with_backslash_in_description = cls() + + model_with_backslash_in_description.additional_properties = d + return model_with_backslash_in_description + + @property + def additional_keys(self) -> List[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index 803bedfeb..11baa0cc5 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -1229,6 +1229,21 @@ } } } + }, + "/tests/description-with-backslash": { + "get": { + "tags": [ + "tests" + ], + "summary": "Test description with \\", + "description": "Test description with \\", + "operationId": "description_with_backslash", + "responses": { + "200": { + "description": "Successful response" + } + } + } } }, "components": { @@ -2223,6 +2238,10 @@ "$ref": "#/components/schemas/AnArrayWithACircularRefInItemsObjectAdditionalPropertiesA" } } + }, + "ModelWithBackslashInDescription": { + "type": "object", + "description": "Description with special character: \\" } }, "parameters": { diff --git a/openapi_python_client/templates/endpoint_macros.py.jinja b/openapi_python_client/templates/endpoint_macros.py.jinja index 4dc0575f9..fde2e018a 100644 --- a/openapi_python_client/templates/endpoint_macros.py.jinja +++ b/openapi_python_client/templates/endpoint_macros.py.jinja @@ -1,4 +1,5 @@ {% from "property_templates/helpers.jinja" import guarded_statement %} +{% from "helpers.jinja" import safe_docstring %} {% macro header_params(endpoint) %} {% if endpoint.header_parameters %} @@ -140,8 +141,8 @@ json_body=json_body, {% endfor %} {% endmacro %} -{% macro docstring(endpoint, return_string) %} -"""{% if endpoint.summary %}{{ endpoint.summary | wordwrap(100)}} +{% macro docstring_content(endpoint, return_string) %} +{% if endpoint.summary %}{{ endpoint.summary | wordwrap(100)}} {% endif -%} {%- if endpoint.description %} {{ endpoint.description | wordwrap(100) }} @@ -165,5 +166,8 @@ Raises: Returns: Response[{{ return_string }}] -""" +{% endmacro %} + +{% macro docstring(endpoint, return_string) %} +{{ safe_docstring(docstring_content(endpoint, return_string)) }} {% endmacro %} diff --git a/openapi_python_client/templates/helpers.jinja b/openapi_python_client/templates/helpers.jinja new file mode 100644 index 000000000..180613c02 --- /dev/null +++ b/openapi_python_client/templates/helpers.jinja @@ -0,0 +1,8 @@ +{% macro safe_docstring(content) %} +{# This macro returns the provided content as a docstring, set to a raw string if it contains a backslash #} +{% if '\\' in content -%} +r""" {{ content }} """ +{%- else -%} +""" {{ content }} """ +{%- endif -%} +{% endmacro %} \ No newline at end of file diff --git a/openapi_python_client/templates/model.py.jinja b/openapi_python_client/templates/model.py.jinja index 98f2d2682..3b2763d68 100644 --- a/openapi_python_client/templates/model.py.jinja +++ b/openapi_python_client/templates/model.py.jinja @@ -31,11 +31,12 @@ if TYPE_CHECKING: {% set class_name = model.class_info.name %} {% set module_name = model.class_info.module_name %} +{% from "helpers.jinja" import safe_docstring %} + T = TypeVar("T", bound="{{ class_name }}") -@attr.s(auto_attribs=True) -class {{ class_name }}: - """{% if model.title %}{{ model.title | wordwrap(116) }} +{% macro class_docstring_content(model) %} + {% if model.title %}{{ model.title | wordwrap(116) }} {% endif -%} {%- if model.description %}{{ model.description | wordwrap(116) }} @@ -55,7 +56,11 @@ class {{ class_name }}: {% for property in model.required_properties + model.optional_properties %} {{ property.to_docstring() | wordwrap(112) | indent(12) }} {% endfor %}{% endif %} - """ +{% endmacro %} + +@attr.s(auto_attribs=True) +class {{ class_name }}: + {{ safe_docstring(class_docstring_content(model)) | indent(4) }} {% for property in model.required_properties + model.optional_properties %} {% if property.default is none and property.required %} diff --git a/openapi_python_client/templates/package_init.py.jinja b/openapi_python_client/templates/package_init.py.jinja index 366a7e508..ecf60e74d 100644 --- a/openapi_python_client/templates/package_init.py.jinja +++ b/openapi_python_client/templates/package_init.py.jinja @@ -1,4 +1,6 @@ -""" {{ package_description }} """ +{% from "helpers.jinja" import safe_docstring %} + +{{ safe_docstring(package_description) }} from .client import AuthenticatedClient, Client __all__ = (