From de20cff2238b80fe8417e589d358f414908fa75b Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 3 Oct 2020 17:00:12 -0400 Subject: [PATCH 01/25] Refactor model building, add e2e section for inline objects. --- .../api/default/ping_ping_get.py | 90 ------------------ .../api/tests/test_inline_objects.py | 68 ++++++++++++++ end_to_end_tests/openapi.json | 60 ++++++++---- openapi_python_client/__init__.py | 4 +- openapi_python_client/parser/model.py | 59 ++++++++++++ openapi_python_client/parser/openapi.py | 60 ++---------- tests/test___init__.py | 2 +- tests/test_openapi_parser/__init__.py | 0 .../default => tests/test_parser}/__init__.py | 0 tests/test_parser/test_model.py | 82 ++++++++++++++++ .../test_openapi.py | 93 ++----------------- .../test_properties.py | 0 .../test_reference.py | 0 .../test_responses.py | 0 14 files changed, 265 insertions(+), 253 deletions(-) delete mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/default/ping_ping_get.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py create mode 100644 openapi_python_client/parser/model.py delete mode 100644 tests/test_openapi_parser/__init__.py rename {end_to_end_tests/golden-record/my_test_api_client/api/default => tests/test_parser}/__init__.py (100%) create mode 100644 tests/test_parser/test_model.py rename tests/{test_openapi_parser => test_parser}/test_openapi.py (88%) rename tests/{test_openapi_parser => test_parser}/test_properties.py (100%) rename tests/{test_openapi_parser => test_parser}/test_reference.py (100%) rename tests/{test_openapi_parser => test_parser}/test_responses.py (100%) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/ping_ping_get.py b/end_to_end_tests/golden-record/my_test_api_client/api/default/ping_ping_get.py deleted file mode 100644 index 9ee1151e0..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/api/default/ping_ping_get.py +++ /dev/null @@ -1,90 +0,0 @@ -from typing import Any, Dict, Optional - -import httpx - -from ...client import Client -from ...types import Response - - -def _get_kwargs( - *, - client: Client, -) -> Dict[str, Any]: - url = "{}/ping".format(client.base_url) - - headers: Dict[str, Any] = client.get_headers() - - return { - "url": url, - "headers": headers, - "cookies": client.get_cookies(), - "timeout": client.get_timeout(), - } - - -def _parse_response(*, response: httpx.Response) -> Optional[bool]: - if response.status_code == 200: - return bool(response.text) - return None - - -def _build_response(*, response: httpx.Response) -> Response[bool]: - return Response( - status_code=response.status_code, - content=response.content, - headers=response.headers, - parsed=_parse_response(response=response), - ) - - -def sync_detailed( - *, - client: Client, -) -> Response[bool]: - kwargs = _get_kwargs( - client=client, - ) - - response = httpx.get( - **kwargs, - ) - - return _build_response(response=response) - - -def sync( - *, - client: Client, -) -> Optional[bool]: - """ A quick check to see if the system is running """ - - return sync_detailed( - client=client, - ).parsed - - -async def asyncio_detailed( - *, - client: Client, -) -> Response[bool]: - kwargs = _get_kwargs( - client=client, - ) - - async with httpx.AsyncClient() as _client: - response = await _client.get(**kwargs) - - return _build_response(response=response) - - -async def asyncio( - *, - client: Client, -) -> Optional[bool]: - """ A quick check to see if the system is running """ - - return ( - await asyncio_detailed( - client=client, - ) - ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py new file mode 100644 index 000000000..1c3ebe3bf --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py @@ -0,0 +1,68 @@ +from typing import Any, Dict + +import httpx + +from ...client import Client +from ...types import Response + + +def _get_kwargs( + *, + client: Client, + json_body: Dict[Any, Any], +) -> Dict[str, Any]: + url = "{}/tests/inline_objects".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + + json_json_body = json_body + + return { + "url": url, + "headers": headers, + "cookies": client.get_cookies(), + "timeout": client.get_timeout(), + "json": json_json_body, + } + + +def _build_response(*, response: httpx.Response) -> Response[None]: + return Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=None, + ) + + +def sync_detailed( + *, + client: Client, + json_body: Dict[Any, Any], +) -> Response[None]: + kwargs = _get_kwargs( + client=client, + json_body=json_body, + ) + + response = httpx.post( + **kwargs, + ) + + return _build_response(response=response) + + +async def asyncio_detailed( + *, + client: Client, + json_body: Dict[Any, Any], +) -> Response[None]: + kwargs = _get_kwargs( + client=client, + json_body=json_body, + ) + + async with httpx.AsyncClient() as _client: + response = await _client.post(**kwargs) + + return _build_response(response=response) diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index ac4aead19..6bc2024f7 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -6,26 +6,6 @@ "version": "0.1.0" }, "paths": { - "/ping": { - "get": { - "summary": "Ping", - "description": "A quick check to see if the system is running ", - "operationId": "ping_ping_get", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "title": "Response Ping Ping Get", - "type": "boolean" - } - } - } - } - } - } - }, "/tests/": { "get": { "tags": ["tests"], @@ -516,6 +496,46 @@ } } } + }, + "/tests/inline_objects": { + "post": { + "tags": ["tests"], + "summary": "Test Inline Objects", + "operationId": "test_inline_objects", + "requestBody": { + "description": "An inline body object", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "a_property": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Inline object response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "a_property": { + "type": "string" + } + } + } + } + } + } + } + } } }, "components": { diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 2fcca23b3..71f5dca83 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -95,7 +95,7 @@ def _get_errors(self) -> Sequence[GeneratorError]: errors = [] for collection in self.openapi.endpoint_collections_by_tag.values(): errors.extend(collection.parse_errors) - errors.extend(self.openapi.schemas.errors) + errors.extend(self.openapi.errors) return errors def _create_package(self) -> None: @@ -148,7 +148,7 @@ def _build_models(self) -> None: imports = [] model_template = self.env.get_template("model.pyi") - for model in self.openapi.schemas.models.values(): + for model in self.openapi.models.values(): module_path = models_dir / f"{model.reference.module_name}.py" module_path.write_text(model_template.render(model=model)) imports.append(import_string_from_reference(model.reference)) diff --git a/openapi_python_client/parser/model.py b/openapi_python_client/parser/model.py new file mode 100644 index 000000000..dff269d6a --- /dev/null +++ b/openapi_python_client/parser/model.py @@ -0,0 +1,59 @@ +""" A Model is used to generate classes from schemas. This module contains Model and helper functions for it """ +from dataclasses import dataclass +from typing import List, Set, Union + +from .. import schema as oai +from .errors import ParseError +from .properties import Property, property_from_data +from .reference import Reference + + +@dataclass +class Model: + """ + A data model used by the API- usually a Schema with type "object". + + These will all be converted to dataclasses in the client + """ + + reference: Reference + required_properties: List[Property] + optional_properties: List[Property] + description: str + relative_imports: Set[str] + + +def model_from_data(*, data: oai.Schema, name: str) -> Union["Model", ParseError]: + """A single Model from its OAI data + + Args: + data: Data of a single Schema + name: Name by which the schema is referenced, such as a model name. + Used to infer the type name if a `title` property is not available. + """ + required_set = set(data.required or []) + required_properties: List[Property] = [] + optional_properties: List[Property] = [] + relative_imports: Set[str] = set() + + ref = Reference.from_ref(data.title or name) + + for key, value in (data.properties or {}).items(): + required = key in required_set + p = property_from_data(name=key, required=required, data=value) + if isinstance(p, ParseError): + return p + if required: + required_properties.append(p) + else: + optional_properties.append(p) + relative_imports.update(p.get_imports(prefix="..")) + + model = Model( + reference=ref, + required_properties=required_properties, + optional_properties=optional_properties, + relative_imports=relative_imports, + description=data.description or "", + ) + return model diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 50966f8a0..666e4b993 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -8,6 +8,7 @@ from .. import schema as oai from .. import utils from .errors import GeneratorError, ParseError, PropertyError +from .model import Model, model_from_data from .properties import EnumProperty, Property, property_from_data from .reference import Reference from .responses import ListRefResponse, RefResponse, Response, response_from_data @@ -220,57 +221,6 @@ def from_data(*, data: oai.Operation, path: str, method: str, tag: str) -> Union return result -@dataclass -class Model: - """ - A data model used by the API- usually a Schema with type "object". - - These will all be converted to dataclasses in the client - """ - - reference: Reference - required_properties: List[Property] - optional_properties: List[Property] - description: str - relative_imports: Set[str] - - @staticmethod - def from_data(*, data: oai.Schema, name: str) -> Union["Model", ParseError]: - """A single Model from its OAI data - - Args: - data: Data of a single Schema - name: Name by which the schema is referenced, such as a model name. - Used to infer the type name if a `title` property is not available. - """ - required_set = set(data.required or []) - required_properties: List[Property] = [] - optional_properties: List[Property] = [] - relative_imports: Set[str] = set() - - ref = Reference.from_ref(data.title or name) - - for key, value in (data.properties or {}).items(): - required = key in required_set - p = property_from_data(name=key, required=required, data=value) - if isinstance(p, ParseError): - return p - if required: - required_properties.append(p) - else: - optional_properties.append(p) - relative_imports.update(p.get_imports(prefix="..")) - - model = Model( - reference=ref, - required_properties=required_properties, - optional_properties=optional_properties, - relative_imports=relative_imports, - description=data.description or "", - ) - return model - - @dataclass class Schemas: """ Contains all the Schemas (references) for an OpenAPI document """ @@ -296,7 +246,7 @@ def build(*, schemas: Dict[str, Union[oai.Reference, oai.Schema]]) -> "Schemas": nullable=data.nullable, ) continue - s = Model.from_data(data=data, name=name) + s = model_from_data(data=data, name=name) if isinstance(s, ParseError): result.errors.append(s) else: @@ -311,7 +261,8 @@ class GeneratorData: title: str description: Optional[str] version: str - schemas: Schemas + models: Dict[str, Model] + errors: List[ParseError] endpoint_collections_by_tag: Dict[str, EndpointCollection] enums: Dict[str, EnumProperty] @@ -334,6 +285,7 @@ def from_dict(d: Dict[str, Dict[str, Any]]) -> Union["GeneratorData", GeneratorE description=openapi.info.description, version=openapi.info.version, endpoint_collections_by_tag=endpoint_collections_by_tag, - schemas=schemas, + models=schemas.models, + errors=schemas.errors, enums=enums, ) diff --git a/tests/test___init__.py b/tests/test___init__.py index d7970e054..d237ebe31 100644 --- a/tests/test___init__.py +++ b/tests/test___init__.py @@ -411,7 +411,7 @@ def test__get_errors(mocker): "default": mocker.MagicMock(autospec=EndpointCollection, parse_errors=[1]), "other": mocker.MagicMock(autospec=EndpointCollection, parse_errors=[2]), }, - schemas=mocker.MagicMock(autospec=Schemas, errors=[3]), + errors=[3], ) project = Project(openapi=openapi) diff --git a/tests/test_openapi_parser/__init__.py b/tests/test_openapi_parser/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/default/__init__.py b/tests/test_parser/__init__.py similarity index 100% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/__init__.py rename to tests/test_parser/__init__.py diff --git a/tests/test_parser/test_model.py b/tests/test_parser/test_model.py new file mode 100644 index 000000000..8322b5431 --- /dev/null +++ b/tests/test_parser/test_model.py @@ -0,0 +1,82 @@ +import openapi_python_client.schema as oai +from openapi_python_client.parser.errors import ParseError + +MODULE_NAME = "openapi_python_client.parser.model" + + +def test_model_from_data(mocker): + from openapi_python_client.parser.properties import Property + + in_data = oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + required=["RequiredEnum"], + properties={ + "RequiredEnum": mocker.MagicMock(), + "OptionalDateTime": mocker.MagicMock(), + }, + ) + required_property = mocker.MagicMock(autospec=Property) + required_imports = mocker.MagicMock() + required_property.get_imports.return_value = {required_imports} + optional_property = mocker.MagicMock(autospec=Property) + optional_imports = mocker.MagicMock() + optional_property.get_imports.return_value = {optional_imports} + property_from_data = mocker.patch( + f"{MODULE_NAME}.property_from_data", + side_effect=[required_property, optional_property], + ) + from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") + + from openapi_python_client.parser.model import Model, model_from_data + + result = model_from_data(data=in_data, name=mocker.MagicMock()) + + from_ref.assert_called_once_with(in_data.title) + property_from_data.assert_has_calls( + [ + mocker.call(name="RequiredEnum", required=True, data=in_data.properties["RequiredEnum"]), + mocker.call(name="OptionalDateTime", required=False, data=in_data.properties["OptionalDateTime"]), + ] + ) + required_property.get_imports.assert_called_once_with(prefix="..") + optional_property.get_imports.assert_called_once_with(prefix="..") + assert result == Model( + reference=from_ref(), + required_properties=[required_property], + optional_properties=[optional_property], + relative_imports={ + required_imports, + optional_imports, + }, + description=in_data.description, + ) + + +def test_model_from_data_property_parse_error(mocker): + in_data = oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + required=["RequiredEnum"], + properties={ + "RequiredEnum": mocker.MagicMock(), + "OptionalDateTime": mocker.MagicMock(), + }, + ) + parse_error = ParseError(data=mocker.MagicMock()) + property_from_data = mocker.patch( + f"{MODULE_NAME}.property_from_data", + return_value=parse_error, + ) + from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") + + from openapi_python_client.parser.model import model_from_data + + result = model_from_data(data=in_data, name=mocker.MagicMock()) + + from_ref.assert_called_once_with(in_data.title) + property_from_data.assert_called_once_with( + name="RequiredEnum", required=True, data=in_data.properties["RequiredEnum"] + ) + + assert result == parse_error diff --git a/tests/test_openapi_parser/test_openapi.py b/tests/test_parser/test_openapi.py similarity index 88% rename from tests/test_openapi_parser/test_openapi.py rename to tests/test_parser/test_openapi.py index d74548e9a..d0fb4a331 100644 --- a/tests/test_openapi_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -1,8 +1,5 @@ -from pydantic import ValidationError -from pydantic.error_wrappers import ErrorWrapper - import openapi_python_client.schema as oai -from openapi_python_client import GeneratorError, utils +from openapi_python_client import GeneratorError from openapi_python_client.parser.errors import ParseError MODULE_NAME = "openapi_python_client.parser.openapi" @@ -31,7 +28,8 @@ def test_from_dict(self, mocker): description=openapi.info.description, version=openapi.info.version, endpoint_collections_by_tag=EndpointCollection.from_data.return_value, - schemas=Schemas.build.return_value, + errors=Schemas.build.return_value.errors, + models=Schemas.build.return_value.models, enums=get_all_enums.return_value, ) @@ -42,7 +40,8 @@ def test_from_dict(self, mocker): generator_data = GeneratorData.from_dict(in_dict) Schemas.build.assert_not_called() - assert generator_data.schemas == Schemas() + assert generator_data.models == Schemas.return_value.models + assert generator_data.errors == Schemas.return_value.errors def test_from_dict_invalid_schema(self, mocker): Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") @@ -67,87 +66,9 @@ def test_from_dict_invalid_schema(self, mocker): Schemas.assert_not_called() -class TestModel: - def test_from_data(self, mocker): - from openapi_python_client.parser.properties import Property - - in_data = oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - required=["RequiredEnum"], - properties={ - "RequiredEnum": mocker.MagicMock(), - "OptionalDateTime": mocker.MagicMock(), - }, - ) - required_property = mocker.MagicMock(autospec=Property) - required_imports = mocker.MagicMock() - required_property.get_imports.return_value = {required_imports} - optional_property = mocker.MagicMock(autospec=Property) - optional_imports = mocker.MagicMock() - optional_property.get_imports.return_value = {optional_imports} - property_from_data = mocker.patch( - f"{MODULE_NAME}.property_from_data", - side_effect=[required_property, optional_property], - ) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") - - from openapi_python_client.parser.openapi import Model - - result = Model.from_data(data=in_data, name=mocker.MagicMock()) - - from_ref.assert_called_once_with(in_data.title) - property_from_data.assert_has_calls( - [ - mocker.call(name="RequiredEnum", required=True, data=in_data.properties["RequiredEnum"]), - mocker.call(name="OptionalDateTime", required=False, data=in_data.properties["OptionalDateTime"]), - ] - ) - required_property.get_imports.assert_called_once_with(prefix="..") - optional_property.get_imports.assert_called_once_with(prefix="..") - assert result == Model( - reference=from_ref(), - required_properties=[required_property], - optional_properties=[optional_property], - relative_imports={ - required_imports, - optional_imports, - }, - description=in_data.description, - ) - - def test_from_data_property_parse_error(self, mocker): - in_data = oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - required=["RequiredEnum"], - properties={ - "RequiredEnum": mocker.MagicMock(), - "OptionalDateTime": mocker.MagicMock(), - }, - ) - parse_error = ParseError(data=mocker.MagicMock()) - property_from_data = mocker.patch( - f"{MODULE_NAME}.property_from_data", - return_value=parse_error, - ) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") - - from openapi_python_client.parser.openapi import Model - - result = Model.from_data(data=in_data, name=mocker.MagicMock()) - - from_ref.assert_called_once_with(in_data.title) - property_from_data.assert_called_once_with( - name="RequiredEnum", required=True, data=in_data.properties["RequiredEnum"] - ) - - assert result == parse_error - - class TestSchemas: def test_build(self, mocker): - from_data = mocker.patch(f"{MODULE_NAME}.Model.from_data") + from_data = mocker.patch(f"{MODULE_NAME}.model_from_data") in_data = {"1": mocker.MagicMock(enum=None), "2": mocker.MagicMock(enum=None), "3": mocker.MagicMock(enum=None)} schema_1 = mocker.MagicMock() schema_2 = mocker.MagicMock() @@ -178,7 +99,7 @@ def test_build_parse_error_on_reference(self): def test_build_enums(self, mocker): from openapi_python_client.parser.openapi import Schemas - from_data = mocker.patch(f"{MODULE_NAME}.Model.from_data") + from_data = mocker.patch(f"{MODULE_NAME}.model_from_data") enum_property = mocker.patch(f"{MODULE_NAME}.EnumProperty") in_data = {"1": mocker.MagicMock(enum=["val1", "val2", "val3"])} diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_parser/test_properties.py similarity index 100% rename from tests/test_openapi_parser/test_properties.py rename to tests/test_parser/test_properties.py diff --git a/tests/test_openapi_parser/test_reference.py b/tests/test_parser/test_reference.py similarity index 100% rename from tests/test_openapi_parser/test_reference.py rename to tests/test_parser/test_reference.py diff --git a/tests/test_openapi_parser/test_responses.py b/tests/test_parser/test_responses.py similarity index 100% rename from tests/test_openapi_parser/test_responses.py rename to tests/test_parser/test_responses.py From 1150b370973faf28e9490266a9d497de0b100b97 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Wed, 14 Oct 2020 18:40:26 -0400 Subject: [PATCH 02/25] [WIP] Major refactor of models/enums/schemas. --- .../api/tests/defaults_tests_defaults_post.py | 45 +- .../api/tests/get_user_list.py | 6 +- .../api/tests/int_enum_tests_int_enum_post.py | 22 +- .../tests/json_body_tests_json_body_post.py | 106 ---- .../api/tests/test_inline_objects.py | 11 +- .../my_test_api_client/models/a_model.py | 100 --- .../my_test_api_client/models/dict_prop.py | 16 + .../models/http_validation_error.py | 38 -- .../my_test_api_client/models/json_body.py | 25 + openapi_python_client/parser/model.py | 59 -- openapi_python_client/parser/openapi.py | 104 ++-- .../{properties.py => properties/__init__.py} | 460 ++++++-------- .../parser/properties/enum_property.py | 91 +++ .../parser/properties/model_property.py | 44 ++ .../parser/properties/property.py | 78 +++ .../parser/properties/schemas.py | 17 + tests/test_parser/__init__.py | 0 tests/test_parser/test_model.py | 82 --- tests/test_parser/test_openapi.py | 301 +++++---- .../test_init.py} | 586 ++++++++++++------ 20 files changed, 1104 insertions(+), 1087 deletions(-) delete mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py delete mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/a_model.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py delete mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/json_body.py delete mode 100644 openapi_python_client/parser/model.py rename openapi_python_client/parser/{properties.py => properties/__init__.py} (50%) create mode 100644 openapi_python_client/parser/properties/enum_property.py create mode 100644 openapi_python_client/parser/properties/model_property.py create mode 100644 openapi_python_client/parser/properties/property.py create mode 100644 openapi_python_client/parser/properties/schemas.py delete mode 100644 tests/test_parser/__init__.py delete mode 100644 tests/test_parser/test_model.py rename tests/test_parser/{test_properties.py => test_properties/test_init.py} (63%) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 6b284cb9c..019b8b45b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -6,6 +6,7 @@ from ...client import Client from ...models.an_enum import AnEnum +from ...models.dict_prop import DictProp from ...models.http_validation_error import HTTPValidationError from ...types import Response @@ -13,7 +14,7 @@ def _get_kwargs( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Optional[str] = "the default string", datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), @@ -22,7 +23,7 @@ def _get_kwargs( boolean_prop: Optional[bool] = False, list_prop: Optional[List[AnEnum]] = None, union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + an_enum: AnEnum, ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) @@ -36,10 +37,10 @@ def _get_kwargs( json_list_prop = None else: json_list_prop = [] - for list_prop_item_data in list_prop: - list_prop_item = list_prop_item_data.value + for an_enum_data in list_prop: + an_enum = an_enum_data.value - json_list_prop.append(list_prop_item) + json_list_prop.append(an_enum) if union_prop is None: json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None @@ -48,9 +49,11 @@ def _get_kwargs( else: json_union_prop = union_prop - json_enum_prop = enum_prop.value if enum_prop else None + json_an_enum = an_enum.value - params: Dict[str, Any] = {} + params: Dict[str, Any] = { + "AnEnum": json_an_enum, + } if string_prop is not None: params["string_prop"] = string_prop if datetime_prop is not None: @@ -67,10 +70,8 @@ def _get_kwargs( params["list_prop"] = json_list_prop if union_prop is not None: params["union_prop"] = json_union_prop - if enum_prop is not None: - params["enum_prop"] = json_enum_prop - json_json_body = json_body + json_json_body = json_body.to_dict() return { "url": url, @@ -102,7 +103,7 @@ def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPVal def sync_detailed( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Optional[str] = "the default string", datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), @@ -111,7 +112,7 @@ def sync_detailed( boolean_prop: Optional[bool] = False, list_prop: Optional[List[AnEnum]] = None, union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + an_enum: AnEnum, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -124,7 +125,7 @@ def sync_detailed( boolean_prop=boolean_prop, list_prop=list_prop, union_prop=union_prop, - enum_prop=enum_prop, + an_enum=an_enum, ) response = httpx.post( @@ -137,7 +138,7 @@ def sync_detailed( def sync( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Optional[str] = "the default string", datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), @@ -146,7 +147,7 @@ def sync( boolean_prop: Optional[bool] = False, list_prop: Optional[List[AnEnum]] = None, union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + an_enum: AnEnum, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -161,14 +162,14 @@ def sync( boolean_prop=boolean_prop, list_prop=list_prop, union_prop=union_prop, - enum_prop=enum_prop, + an_enum=an_enum, ).parsed async def asyncio_detailed( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Optional[str] = "the default string", datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), @@ -177,7 +178,7 @@ async def asyncio_detailed( boolean_prop: Optional[bool] = False, list_prop: Optional[List[AnEnum]] = None, union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + an_enum: AnEnum, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -190,7 +191,7 @@ async def asyncio_detailed( boolean_prop=boolean_prop, list_prop=list_prop, union_prop=union_prop, - enum_prop=enum_prop, + an_enum=an_enum, ) async with httpx.AsyncClient() as _client: @@ -202,7 +203,7 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Optional[str] = "the default string", datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), @@ -211,7 +212,7 @@ async def asyncio( boolean_prop: Optional[bool] = False, list_prop: Optional[List[AnEnum]] = None, union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + an_enum: AnEnum, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -227,6 +228,6 @@ async def asyncio( boolean_prop=boolean_prop, list_prop=list_prop, union_prop=union_prop, - enum_prop=enum_prop, + an_enum=an_enum, ) ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py index 815e0b02e..14b0327fb 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py @@ -21,10 +21,10 @@ def _get_kwargs( headers: Dict[str, Any] = client.get_headers() json_an_enum_value = [] - for an_enum_value_item_data in an_enum_value: - an_enum_value_item = an_enum_value_item_data.value + for an_enum_data in an_enum_value: + an_enum = an_enum_data.value - json_an_enum_value.append(an_enum_value_item) + json_an_enum_value.append(an_enum) if isinstance(some_date, datetime.date): json_some_date = some_date.isoformat() diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py index e15ce2e2c..1ea8a7453 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py @@ -11,16 +11,16 @@ def _get_kwargs( *, client: Client, - int_enum: AnIntEnum, + an_int_enum: AnIntEnum, ) -> Dict[str, Any]: url = "{}/tests/int_enum".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - json_int_enum = int_enum.value + json_an_int_enum = an_int_enum.value params: Dict[str, Any] = { - "int_enum": json_int_enum, + "AnIntEnum": json_an_int_enum, } return { @@ -52,11 +52,11 @@ def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPVal def sync_detailed( *, client: Client, - int_enum: AnIntEnum, + an_int_enum: AnIntEnum, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, - int_enum=int_enum, + an_int_enum=an_int_enum, ) response = httpx.post( @@ -69,24 +69,24 @@ def sync_detailed( def sync( *, client: Client, - int_enum: AnIntEnum, + an_int_enum: AnIntEnum, ) -> Optional[Union[None, HTTPValidationError]]: """ """ return sync_detailed( client=client, - int_enum=int_enum, + an_int_enum=an_int_enum, ).parsed async def asyncio_detailed( *, client: Client, - int_enum: AnIntEnum, + an_int_enum: AnIntEnum, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, - int_enum=int_enum, + an_int_enum=an_int_enum, ) async with httpx.AsyncClient() as _client: @@ -98,13 +98,13 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - int_enum: AnIntEnum, + an_int_enum: AnIntEnum, ) -> Optional[Union[None, HTTPValidationError]]: """ """ return ( await asyncio_detailed( client=client, - int_enum=int_enum, + an_int_enum=an_int_enum, ) ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py deleted file mode 100644 index eb556c5d7..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py +++ /dev/null @@ -1,106 +0,0 @@ -from typing import Any, Dict, Optional, Union, cast - -import httpx - -from ...client import Client -from ...models.a_model import AModel -from ...models.http_validation_error import HTTPValidationError -from ...types import Response - - -def _get_kwargs( - *, - client: Client, - json_body: AModel, -) -> Dict[str, Any]: - url = "{}/tests/json_body".format(client.base_url) - - headers: Dict[str, Any] = client.get_headers() - - json_json_body = json_body.to_dict() - - return { - "url": url, - "headers": headers, - "cookies": client.get_cookies(), - "timeout": client.get_timeout(), - "json": json_json_body, - } - - -def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: - if response.status_code == 200: - return None - if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) - return None - - -def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPValidationError]]: - return Response( - status_code=response.status_code, - content=response.content, - headers=response.headers, - parsed=_parse_response(response=response), - ) - - -def sync_detailed( - *, - client: Client, - json_body: AModel, -) -> Response[Union[None, HTTPValidationError]]: - kwargs = _get_kwargs( - client=client, - json_body=json_body, - ) - - response = httpx.post( - **kwargs, - ) - - return _build_response(response=response) - - -def sync( - *, - client: Client, - json_body: AModel, -) -> Optional[Union[None, HTTPValidationError]]: - """ Try sending a JSON body """ - - return sync_detailed( - client=client, - json_body=json_body, - ).parsed - - -async def asyncio_detailed( - *, - client: Client, - json_body: AModel, -) -> Response[Union[None, HTTPValidationError]]: - kwargs = _get_kwargs( - client=client, - json_body=json_body, - ) - - async with httpx.AsyncClient() as _client: - response = await _client.post(**kwargs) - - return _build_response(response=response) - - -async def asyncio( - *, - client: Client, - json_body: AModel, -) -> Optional[Union[None, HTTPValidationError]]: - """ Try sending a JSON body """ - - return ( - await asyncio_detailed( - client=client, - json_body=json_body, - ) - ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py index 1c3ebe3bf..56bec3cfa 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py @@ -1,21 +1,22 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional import httpx from ...client import Client +from ...models.json_body import JsonBody from ...types import Response def _get_kwargs( *, client: Client, - json_body: Dict[Any, Any], + json_body: Optional[JsonBody], ) -> Dict[str, Any]: url = "{}/tests/inline_objects".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - json_json_body = json_body + json_json_body = json_body.to_dict() if json_body else None return { "url": url, @@ -38,7 +39,7 @@ def _build_response(*, response: httpx.Response) -> Response[None]: def sync_detailed( *, client: Client, - json_body: Dict[Any, Any], + json_body: Optional[JsonBody], ) -> Response[None]: kwargs = _get_kwargs( client=client, @@ -55,7 +56,7 @@ def sync_detailed( async def asyncio_detailed( *, client: Client, - json_body: Dict[Any, Any], + json_body: Optional[JsonBody], ) -> Response[None]: kwargs = _get_kwargs( client=client, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py deleted file mode 100644 index a1a0ace0c..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ /dev/null @@ -1,100 +0,0 @@ -import datetime -from typing import Any, Dict, List, Optional, Union - -import attr -from dateutil.parser import isoparse - -from ..models.an_enum import AnEnum -from ..models.different_enum import DifferentEnum - - -@attr.s(auto_attribs=True) -class AModel: - """ A Model for testing all the ways custom objects can be used """ - - an_enum_value: AnEnum - some_dict: Optional[Dict[Any, Any]] - a_camel_date_time: Union[datetime.datetime, datetime.date] - a_date: datetime.date - nested_list_of_enums: Optional[List[List[DifferentEnum]]] = None - attr_1_leading_digit: Optional[str] = None - - def to_dict(self) -> Dict[str, Any]: - an_enum_value = self.an_enum_value.value - - some_dict = self.some_dict - - if isinstance(self.a_camel_date_time, datetime.datetime): - a_camel_date_time = self.a_camel_date_time.isoformat() - - else: - a_camel_date_time = self.a_camel_date_time.isoformat() - - a_date = self.a_date.isoformat() - - if self.nested_list_of_enums is None: - nested_list_of_enums = None - else: - nested_list_of_enums = [] - for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value - - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - nested_list_of_enums.append(nested_list_of_enums_item) - - attr_1_leading_digit = self.attr_1_leading_digit - - return { - "an_enum_value": an_enum_value, - "some_dict": some_dict, - "aCamelDateTime": a_camel_date_time, - "a_date": a_date, - "nested_list_of_enums": nested_list_of_enums, - "1_leading_digit": attr_1_leading_digit, - } - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "AModel": - an_enum_value = AnEnum(d["an_enum_value"]) - - some_dict = d["some_dict"] - - def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]: - a_camel_date_time: Union[datetime.datetime, datetime.date] - try: - a_camel_date_time = isoparse(d["aCamelDateTime"]) - - return a_camel_date_time - except: # noqa: E722 - pass - a_camel_date_time = isoparse(d["aCamelDateTime"]).date() - - return a_camel_date_time - - a_camel_date_time = _parse_a_camel_date_time(d["aCamelDateTime"]) - - a_date = isoparse(d["a_date"]).date() - - nested_list_of_enums = [] - for nested_list_of_enums_item_data in d.get("nested_list_of_enums") or []: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) - - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - nested_list_of_enums.append(nested_list_of_enums_item) - - attr_1_leading_digit = d.get("1_leading_digit") - - return AModel( - an_enum_value=an_enum_value, - some_dict=some_dict, - a_camel_date_time=a_camel_date_time, - a_date=a_date, - nested_list_of_enums=nested_list_of_enums, - attr_1_leading_digit=attr_1_leading_digit, - ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py b/end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py new file mode 100644 index 000000000..37d2bfad9 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py @@ -0,0 +1,16 @@ +from typing import Any, Dict + +import attr + + +@attr.s(auto_attribs=True) +class DictProp: + """ """ + + def to_dict(self) -> Dict[str, Any]: + + return {} + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "DictProp": + return DictProp() diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py deleted file mode 100644 index 90cd71e8c..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ /dev/null @@ -1,38 +0,0 @@ -from typing import Any, Dict, List, Optional - -import attr - -from ..models.validation_error import ValidationError - - -@attr.s(auto_attribs=True) -class HTTPValidationError: - """ """ - - detail: Optional[List[ValidationError]] = None - - def to_dict(self) -> Dict[str, Any]: - if self.detail is None: - detail = None - else: - detail = [] - for detail_item_data in self.detail: - detail_item = detail_item_data.to_dict() - - detail.append(detail_item) - - return { - "detail": detail, - } - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": - detail = [] - for detail_item_data in d.get("detail") or []: - detail_item = ValidationError.from_dict(detail_item_data) - - detail.append(detail_item) - - return HTTPValidationError( - detail=detail, - ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py b/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py new file mode 100644 index 000000000..4cbed755d --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py @@ -0,0 +1,25 @@ +from typing import Any, Dict, Optional + +import attr + + +@attr.s(auto_attribs=True) +class JsonBody: + """ """ + + a_property: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + a_property = self.a_property + + return { + "a_property": a_property, + } + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "JsonBody": + a_property = d.get("a_property") + + return JsonBody( + a_property=a_property, + ) diff --git a/openapi_python_client/parser/model.py b/openapi_python_client/parser/model.py deleted file mode 100644 index dff269d6a..000000000 --- a/openapi_python_client/parser/model.py +++ /dev/null @@ -1,59 +0,0 @@ -""" A Model is used to generate classes from schemas. This module contains Model and helper functions for it """ -from dataclasses import dataclass -from typing import List, Set, Union - -from .. import schema as oai -from .errors import ParseError -from .properties import Property, property_from_data -from .reference import Reference - - -@dataclass -class Model: - """ - A data model used by the API- usually a Schema with type "object". - - These will all be converted to dataclasses in the client - """ - - reference: Reference - required_properties: List[Property] - optional_properties: List[Property] - description: str - relative_imports: Set[str] - - -def model_from_data(*, data: oai.Schema, name: str) -> Union["Model", ParseError]: - """A single Model from its OAI data - - Args: - data: Data of a single Schema - name: Name by which the schema is referenced, such as a model name. - Used to infer the type name if a `title` property is not available. - """ - required_set = set(data.required or []) - required_properties: List[Property] = [] - optional_properties: List[Property] = [] - relative_imports: Set[str] = set() - - ref = Reference.from_ref(data.title or name) - - for key, value in (data.properties or {}).items(): - required = key in required_set - p = property_from_data(name=key, required=required, data=value) - if isinstance(p, ParseError): - return p - if required: - required_properties.append(p) - else: - optional_properties.append(p) - relative_imports.update(p.get_imports(prefix="..")) - - model = Model( - reference=ref, - required_properties=required_properties, - optional_properties=optional_properties, - relative_imports=relative_imports, - description=data.description or "", - ) - return model diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 666e4b993..5c300097e 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -1,15 +1,14 @@ from copy import deepcopy from dataclasses import dataclass, field from enum import Enum -from typing import Any, Dict, List, Optional, Set, Union +from typing import Any, Dict, List, Optional, Set, Tuple, Union from pydantic import ValidationError from .. import schema as oai from .. import utils from .errors import GeneratorError, ParseError, PropertyError -from .model import Model, model_from_data -from .properties import EnumProperty, Property, property_from_data +from .properties import EnumProperty, ModelProperty, Property, Schemas, build_schemas, property_from_data from .reference import Reference from .responses import ListRefResponse, RefResponse, Response, response_from_data @@ -36,7 +35,9 @@ class EndpointCollection: parse_errors: List[ParseError] = field(default_factory=list) @staticmethod - def from_data(*, data: Dict[str, oai.PathItem]) -> Dict[str, "EndpointCollection"]: + def from_data( + *, data: Dict[str, oai.PathItem], schemas: Schemas + ) -> Tuple[Dict[str, "EndpointCollection"], Schemas]: """ Parse the openapi paths data to get EndpointCollections by tag """ endpoints_by_tag: Dict[str, EndpointCollection] = {} @@ -49,7 +50,9 @@ def from_data(*, data: Dict[str, oai.PathItem]) -> Dict[str, "EndpointCollection continue tag = (operation.tags or ["default"])[0] collection = endpoints_by_tag.setdefault(tag, EndpointCollection(tag=tag)) - endpoint = Endpoint.from_data(data=operation, path=path, method=method, tag=tag) + endpoint, schemas = Endpoint.from_data( + data=operation, path=path, method=method, tag=tag, schemas=schemas + ) if isinstance(endpoint, ParseError): endpoint.header = ( f"ERROR parsing {method.upper()} {path} within {tag}. Endpoint will not be generated." @@ -61,7 +64,7 @@ def from_data(*, data: Dict[str, oai.PathItem]) -> Dict[str, "EndpointCollection collection.parse_errors.append(error) collection.endpoints.append(endpoint) - return endpoints_by_tag + return endpoints_by_tag, schemas def generate_operation_id(*, path: str, method: str) -> str: @@ -115,25 +118,29 @@ def parse_multipart_body(body: oai.RequestBody) -> Optional[Reference]: return None @staticmethod - def parse_request_json_body(body: oai.RequestBody) -> Union[Property, PropertyError, None]: + def parse_request_json_body( + *, body: oai.RequestBody, schemas: Schemas + ) -> Tuple[Union[Property, PropertyError, None], Schemas]: """ Return json_body """ body_content = body.content json_body = body_content.get("application/json") if json_body is not None and json_body.media_type_schema is not None: - return property_from_data("json_body", required=True, data=json_body.media_type_schema) - return None + return property_from_data("json_body", required=True, data=json_body.media_type_schema, schemas=schemas) + return None, schemas @staticmethod - def _add_body(endpoint: "Endpoint", data: oai.Operation) -> Union[ParseError, "Endpoint"]: + def _add_body( + *, endpoint: "Endpoint", data: oai.Operation, schemas: Schemas + ) -> Tuple[Union[ParseError, "Endpoint"], Schemas]: """ Adds form or JSON body to Endpoint if included in data """ endpoint = deepcopy(endpoint) if data.requestBody is None or isinstance(data.requestBody, oai.Reference): - return endpoint + return endpoint, schemas endpoint.form_body_reference = Endpoint.parse_request_form_body(data.requestBody) - json_body = Endpoint.parse_request_json_body(data.requestBody) + json_body, schemas = Endpoint.parse_request_json_body(body=data.requestBody, schemas=schemas) if isinstance(json_body, ParseError): - return ParseError(detail=f"cannot parse body of endpoint {endpoint.name}", data=json_body.data) + return ParseError(detail=f"cannot parse body of endpoint {endpoint.name}", data=json_body.data), schemas endpoint.multipart_body_reference = Endpoint.parse_multipart_body(data.requestBody) @@ -148,7 +155,7 @@ def _add_body(endpoint: "Endpoint", data: oai.Operation) -> Union[ParseError, "E if json_body is not None: endpoint.json_body = json_body endpoint.relative_imports.update(endpoint.json_body.get_imports(prefix="...")) - return endpoint + return endpoint, schemas @staticmethod def _add_responses(endpoint: "Endpoint", data: oai.Responses) -> "Endpoint": @@ -172,16 +179,20 @@ def _add_responses(endpoint: "Endpoint", data: oai.Responses) -> "Endpoint": return endpoint @staticmethod - def _add_parameters(endpoint: "Endpoint", data: oai.Operation) -> Union["Endpoint", ParseError]: + def _add_parameters( + *, endpoint: "Endpoint", data: oai.Operation, schemas: Schemas + ) -> Tuple[Union["Endpoint", ParseError], Schemas]: endpoint = deepcopy(endpoint) if data.parameters is None: - return endpoint + return endpoint, schemas for param in data.parameters: if isinstance(param, oai.Reference) or param.param_schema is None: continue - prop = property_from_data(name=param.name, required=param.required, data=param.param_schema) + prop, schemas = property_from_data( + name=param.name, required=param.required, data=param.param_schema, schemas=schemas + ) if isinstance(prop, ParseError): - return ParseError(detail=f"cannot parse parameter of endpoint {endpoint.name}", data=prop.data) + return ParseError(detail=f"cannot parse parameter of endpoint {endpoint.name}", data=prop.data), schemas endpoint.relative_imports.update(prop.get_imports(prefix="...")) if param.param_in == ParameterLocation.QUERY: @@ -191,11 +202,13 @@ def _add_parameters(endpoint: "Endpoint", data: oai.Operation) -> Union["Endpoin elif param.param_in == ParameterLocation.HEADER: endpoint.header_parameters.append(prop) else: - return ParseError(data=param, detail="Parameter must be declared in path or query") - return endpoint + return ParseError(data=param, detail="Parameter must be declared in path or query"), schemas + return endpoint, schemas @staticmethod - def from_data(*, data: oai.Operation, path: str, method: str, tag: str) -> Union["Endpoint", ParseError]: + def from_data( + *, data: oai.Operation, path: str, method: str, tag: str, schemas: Schemas + ) -> Tuple[Union["Endpoint", ParseError], Schemas]: """ Construct an endpoint from the OpenAPI data """ if data.operationId is None: @@ -212,46 +225,13 @@ def from_data(*, data: oai.Operation, path: str, method: str, tag: str) -> Union tag=tag, ) - result = Endpoint._add_parameters(endpoint, data) + result, schemas = Endpoint._add_parameters(endpoint=endpoint, data=data, schemas=schemas) if isinstance(result, ParseError): - return result + return result, schemas result = Endpoint._add_responses(result, data.responses) - result = Endpoint._add_body(result, data) - - return result - - -@dataclass -class Schemas: - """ Contains all the Schemas (references) for an OpenAPI document """ - - models: Dict[str, Model] = field(default_factory=dict) - errors: List[ParseError] = field(default_factory=list) + result, schemas = Endpoint._add_body(endpoint=result, data=data, schemas=schemas) - @staticmethod - def build(*, schemas: Dict[str, Union[oai.Reference, oai.Schema]]) -> "Schemas": - """ Get a list of Schemas from an OpenAPI dict """ - result = Schemas() - for name, data in schemas.items(): - if isinstance(data, oai.Reference): - result.errors.append(ParseError(data=data, detail="Reference schemas are not supported.")) - continue - if data.enum is not None: - EnumProperty( - name=name, - title=data.title or name, - required=True, - default=data.default, - values=EnumProperty.values_from_list(data.enum), - nullable=data.nullable, - ) - continue - s = model_from_data(data=data, name=name) - if isinstance(s, ParseError): - result.errors.append(s) - else: - result.models[s.reference.class_name] = s - return result + return result, schemas @dataclass @@ -261,7 +241,7 @@ class GeneratorData: title: str description: Optional[str] version: str - models: Dict[str, Model] + models: Dict[str, ModelProperty] errors: List[ParseError] endpoint_collections_by_tag: Dict[str, EndpointCollection] enums: Dict[str, EnumProperty] @@ -276,9 +256,9 @@ def from_dict(d: Dict[str, Dict[str, Any]]) -> Union["GeneratorData", GeneratorE if openapi.components is None or openapi.components.schemas is None: schemas = Schemas() else: - schemas = Schemas.build(schemas=openapi.components.schemas) - endpoint_collections_by_tag = EndpointCollection.from_data(data=openapi.paths) - enums = EnumProperty.get_all_enums() + schemas = build_schemas(components=openapi.components.schemas) + endpoint_collections_by_tag, schemas = EndpointCollection.from_data(data=openapi.paths, schemas=schemas) + enums = schemas.enums return GeneratorData( title=openapi.info.title, diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties/__init__.py similarity index 50% rename from openapi_python_client/parser/properties.py rename to openapi_python_client/parser/properties/__init__.py index afc1711ec..c8c2e7239 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -1,85 +1,17 @@ -from dataclasses import InitVar, dataclass, field +from dataclasses import dataclass, replace from itertools import chain -from typing import Any, ClassVar, Dict, Generic, List, Optional, Set, Type, TypeVar, Union +from typing import Any, ClassVar, Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union from dateutil.parser import isoparse -from .. import schema as oai -from .. import utils -from .errors import PropertyError, ValidationError -from .reference import Reference - - -@dataclass -class Property: - """ - Describes a single property for a schema - - Attributes: - template: Name of the template file (if any) to use for this property. Must be stored in - templates/property_templates and must contain two macros: construct and transform. Construct will be used to - build this property from JSON data (a response from an API). Transform will be used to convert this property - to JSON data (when sending a request to the API). - - Raises: - ValidationError: Raised when the default value fails to be converted to the expected type - """ - - name: str - required: bool - nullable: bool - default: Optional[Any] - - template: ClassVar[Optional[str]] = None - _type_string: ClassVar[str] - - python_name: str = field(init=False) - - def __post_init__(self) -> None: - self.python_name = utils.to_valid_python_identifier(utils.snake_case(self.name)) - if self.default is not None: - self.default = self._validate_default(default=self.default) - - def _validate_default(self, default: Any) -> Any: - """ Check that the default value is valid for the property's type + perform any necessary sanitization """ - raise ValidationError - - def get_type_string(self, no_optional: bool = False) -> str: - """ - Get a string representation of type that should be used when declaring this property - - Args: - no_optional: Do not include Optional even if the value is optional (needed for isinstance checks) - """ - if no_optional or (self.required and not self.nullable): - return self._type_string - return f"Optional[{self._type_string}]" - - def get_imports(self, *, prefix: str) -> Set[str]: - """ - Get a set of import strings that should be included when this property is used somewhere - - Args: - prefix: A prefix to put before any relative (local) module names. This should be the number of . to get - back to the root of the generated client. - """ - if self.nullable or not self.required: - return {"from typing import Optional"} - return set() - - def to_string(self) -> str: - """ How this should be declared in a dataclass """ - if self.default: - default = self.default - elif not self.required: - default = "None" - else: - default = None - - if default is not None: - return f"{self.python_name}: {self.get_type_string()} = {self.default}" - else: - return f"{self.python_name}: {self.get_type_string()}" +from ... import schema as oai +from ... import utils +from ..errors import PropertyError, ValidationError +from ..reference import Reference +from .enum_property import EnumProperty +from .model_property import ModelProperty +from .property import Property +from .schemas import Schemas @dataclass @@ -282,144 +214,6 @@ def _validate_default(self, default: Any) -> Any: raise ValidationError() -_existing_enums: Dict[str, "EnumProperty"] = {} -ValueType = Union[str, int] - - -@dataclass -class EnumProperty(Property): - """ A property that should use an enum """ - - values: Dict[str, ValueType] - reference: Reference = field(init=False) - title: InitVar[str] - value_type: Type[ValueType] = field(init=False) - - template: ClassVar[str] = "enum_property.pyi" - - def __post_init__(self, title: str) -> None: # type: ignore - reference = Reference.from_ref(title) - dedup_counter = 0 - while reference.class_name in _existing_enums: - existing = _existing_enums[reference.class_name] - if self.values == existing.values: - break # This is the same Enum, we're good - dedup_counter += 1 - reference = Reference.from_ref(f"{reference.class_name}{dedup_counter}") - - self.reference = reference - - for value in self.values.values(): - self.value_type = type(value) - break - - super().__post_init__() - _existing_enums[self.reference.class_name] = self - - @staticmethod - def get_all_enums() -> Dict[str, "EnumProperty"]: - """ Get all the EnumProperties that have been registered keyed by class name """ - return _existing_enums - - @staticmethod - def get_enum(name: str) -> Optional["EnumProperty"]: - """ Get all the EnumProperties that have been registered keyed by class name """ - return _existing_enums.get(name) - - def get_type_string(self, no_optional: bool = False) -> str: - """ Get a string representation of type that should be used when declaring this property """ - - if no_optional or (self.required and not self.nullable): - return self.reference.class_name - return f"Optional[{self.reference.class_name}]" - - def get_imports(self, *, prefix: str) -> Set[str]: - """ - Get a set of import strings that should be included when this property is used somewhere - - Args: - prefix: A prefix to put before any relative (local) module names. This should be the number of . to get - back to the root of the generated client. - """ - imports = super().get_imports(prefix=prefix) - imports.add(f"from {prefix}models.{self.reference.module_name} import {self.reference.class_name}") - return imports - - @staticmethod - def values_from_list(values: List[ValueType]) -> Dict[str, ValueType]: - """ Convert a list of values into dict of {name: value} """ - output: Dict[str, ValueType] = {} - - for i, value in enumerate(values): - if isinstance(value, int): - if value < 0: - output[f"VALUE_NEGATIVE_{-value}"] = value - else: - output[f"VALUE_{value}"] = value - continue - if value[0].isalpha(): - key = value.upper() - else: - key = f"VALUE_{i}" - if key in output: - raise ValueError(f"Duplicate key {key} in Enum") - sanitized_key = utils.snake_case(key).upper() - output[sanitized_key] = utils.remove_string_escapes(value) - return output - - def _validate_default(self, default: Any) -> str: - inverse_values = {v: k for k, v in self.values.items()} - try: - return f"{self.reference.class_name}.{inverse_values[default]}" - except KeyError as e: - raise ValidationError() from e - - -@dataclass -class RefProperty(Property): - """ A property which refers to another Schema """ - - reference: Reference - - @property - def template(self) -> str: # type: ignore - enum = EnumProperty.get_enum(self.reference.class_name) - if enum: - return "enum_property.pyi" - return "ref_property.pyi" - - def get_type_string(self, no_optional: bool = False) -> str: - """ Get a string representation of type that should be used when declaring this property """ - if no_optional or (self.required and not self.nullable): - return self.reference.class_name - return f"Optional[{self.reference.class_name}]" - - def get_imports(self, *, prefix: str) -> Set[str]: - """ - Get a set of import strings that should be included when this property is used somewhere - - Args: - prefix: A prefix to put before any relative (local) module names. This should be the number of . to get - back to the root of the generated client. - """ - imports = super().get_imports(prefix=prefix) - imports.update( - { - f"from {prefix}models.{self.reference.module_name} import {self.reference.class_name}", - "from typing import Dict", - "from typing import cast", - } - ) - return imports - - def _validate_default(self, default: Any) -> Any: - enum = EnumProperty.get_enum(self.reference.class_name) - if enum: - return enum._validate_default(default) - else: - raise ValidationError - - @dataclass class DictProperty(Property): """ Property that is a general Dict """ @@ -482,94 +276,192 @@ def _string_based_property( ) -def _property_from_data( - name: str, required: bool, data: Union[oai.Reference, oai.Schema] -) -> Union[Property, PropertyError]: - """ Generate a Property from the OpenAPI dictionary representation of it """ - name = utils.remove_string_escapes(name) - if isinstance(data, oai.Reference): - return RefProperty( - name=name, - required=required, - reference=Reference.from_ref(data.ref), - default=None, - nullable=False, - ) - if data.enum: - return EnumProperty( +def build_model_property( + *, data: oai.Schema, name: str, schemas: Schemas, required: bool +) -> Tuple[Union[ModelProperty, PropertyError], Schemas]: + """ + A single ModelProperty from its OAI data + + Args: + data: Data of a single Schema + name: Name by which the schema is referenced, such as a model name. + Used to infer the type name if a `title` property is not available. + schemas: Existing Schemas which have already been processed (to check name conflicts) + """ + required_set = set(data.required or []) + required_properties: List[Property] = [] + optional_properties: List[Property] = [] + relative_imports: Set[str] = set() + + ref = Reference.from_ref(data.title or name) + + for key, value in (data.properties or {}).items(): + required = key in required_set + prop, schemas = property_from_data(name=key, required=required, data=value, schemas=schemas) + if isinstance(prop, PropertyError): + return prop, schemas + if required: + required_properties.append(prop) + else: + optional_properties.append(prop) + relative_imports.update(prop.get_imports(prefix="..")) + + prop = ModelProperty( + reference=ref, + required_properties=required_properties, + optional_properties=optional_properties, + relative_imports=relative_imports, + description=data.description or "", + default=None, + nullable=data.nullable, + required=required, + name=name, + ) + schemas = replace(schemas, models={**schemas.models, prop.reference.class_name: prop}) + return prop, schemas + + +def build_enum_property( + *, data: oai.Schema, name: str, required: bool, schemas: Schemas, enum: List[Union[str, int]] +) -> Tuple[EnumProperty, Schemas]: + prop = EnumProperty( + name=name, + required=required, + values=EnumProperty.values_from_list(enum), + title=data.title or name, + default=data.default, + nullable=data.nullable, + existing_enums=schemas.enums, + ) + schemas = replace(schemas, enums={**schemas.enums, prop.reference.class_name: prop}) + return prop, schemas + + +def build_union_property( + *, data: oai.Schema, name: str, required: bool, schemas: Schemas +) -> Tuple[Union[UnionProperty, PropertyError], Schemas]: + sub_properties: List[Property] = [] + for sub_prop_data in chain(data.anyOf, data.oneOf): + sub_prop, schemas = property_from_data(name=name, required=required, data=sub_prop_data, schemas=schemas) + if isinstance(sub_prop, PropertyError): + return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), schemas + sub_properties.append(sub_prop) + return ( + UnionProperty( name=name, required=required, - values=EnumProperty.values_from_list(data.enum), - title=data.title or name, default=data.default, + inner_properties=sub_properties, nullable=data.nullable, - ) - if data.anyOf or data.oneOf: - sub_properties: List[Property] = [] - for sub_prop_data in chain(data.anyOf, data.oneOf): - sub_prop = property_from_data(name=name, required=required, data=sub_prop_data) - if isinstance(sub_prop, PropertyError): - return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data) - sub_properties.append(sub_prop) - return UnionProperty( + ), + schemas, + ) + + +def build_list_property( + *, data: oai.Schema, name: str, required: bool, schemas: Schemas +) -> Tuple[Union[ListProperty[Any], PropertyError], Schemas]: + if data.items is None: + return PropertyError(data=data, detail="type array must have items defined"), schemas + inner_prop, schemas = property_from_data(name=f"{name}_item", required=True, data=data.items, schemas=schemas) + if isinstance(inner_prop, PropertyError): + return PropertyError(data=inner_prop.data, detail=f"invalid data in items of array {name}"), schemas + return ( + ListProperty( name=name, required=required, default=data.default, - inner_properties=sub_properties, + inner_property=inner_prop, nullable=data.nullable, - ) + ), + schemas, + ) + + +def _property_from_data( + name: str, + required: bool, + data: Union[oai.Reference, oai.Schema], + schemas: Schemas, +) -> Tuple[Union[Property, PropertyError], Schemas]: + """ Generate a Property from the OpenAPI dictionary representation of it """ + name = utils.remove_string_escapes(name) + if isinstance(data, oai.Reference): + reference = Reference.from_ref(data.ref) + if reference.class_name in schemas.enums: + return schemas.enums[reference.class_name], schemas + elif reference.class_name in schemas.models: + return schemas.models[reference.class_name], schemas + else: + return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas + if data.enum: + return build_enum_property(data=data, name=name, required=required, schemas=schemas, enum=data.enum) + if data.anyOf or data.oneOf: + return build_union_property(data=data, name=name, required=required, schemas=schemas) if not data.type: - return PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined.") + return PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined."), schemas if data.type == "string": - return _string_based_property(name=name, required=required, data=data) + return _string_based_property(name=name, required=required, data=data), schemas elif data.type == "number": - return FloatProperty( - name=name, - default=data.default, - required=required, - nullable=data.nullable, + return ( + FloatProperty( + name=name, + default=data.default, + required=required, + nullable=data.nullable, + ), + schemas, ) elif data.type == "integer": - return IntProperty( - name=name, - default=data.default, - required=required, - nullable=data.nullable, + return ( + IntProperty( + name=name, + default=data.default, + required=required, + nullable=data.nullable, + ), + schemas, ) elif data.type == "boolean": - return BooleanProperty( - name=name, - required=required, - default=data.default, - nullable=data.nullable, + return ( + BooleanProperty( + name=name, + required=required, + default=data.default, + nullable=data.nullable, + ), + schemas, ) elif data.type == "array": - if data.items is None: - return PropertyError(data=data, detail="type array must have items defined") - inner_prop = property_from_data(name=f"{name}_item", required=True, data=data.items) - if isinstance(inner_prop, PropertyError): - return PropertyError(data=inner_prop.data, detail=f"invalid data in items of array {name}") - return ListProperty( - name=name, - required=required, - default=data.default, - inner_property=inner_prop, - nullable=data.nullable, - ) + return build_list_property(data=data, name=name, required=required, schemas=schemas) elif data.type == "object": - return DictProperty( - name=name, - required=required, - default=data.default, - nullable=data.nullable, - ) - return PropertyError(data=data, detail=f"unknown type {data.type}") + return build_model_property(data=data, name=name, schemas=schemas, required=required) + return PropertyError(data=data, detail=f"unknown type {data.type}"), schemas def property_from_data( - name: str, required: bool, data: Union[oai.Reference, oai.Schema] -) -> Union[Property, PropertyError]: + name: str, + required: bool, + data: Union[oai.Reference, oai.Schema], + schemas: Schemas, +) -> Tuple[Union[Property, PropertyError], Schemas]: try: - return _property_from_data(name=name, required=required, data=data) + return _property_from_data(name=name, required=required, data=data, schemas=schemas) except ValidationError: - return PropertyError(detail="Failed to validate default value", data=data) + return PropertyError(detail="Failed to validate default value", data=data), schemas + + +def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) -> Schemas: + """ Get a list of Schemas from an OpenAPI dict """ + schemas = Schemas() + for name, data in components.items(): + if isinstance(data, oai.Reference): + schemas.errors.append(PropertyError(data=data, detail="Reference schemas are not supported.")) + continue + if data.enum is not None: + prop, schemas = build_enum_property(data=data, name=name, required=True, schemas=schemas, enum=data.enum) + continue + model, schemas = build_model_property(data=data, name=name, schemas=schemas, required=True) + if isinstance(model, PropertyError): + schemas.errors.append(model) + return schemas diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py new file mode 100644 index 000000000..852f03c73 --- /dev/null +++ b/openapi_python_client/parser/properties/enum_property.py @@ -0,0 +1,91 @@ +__all__ = ["EnumProperty"] + +from dataclasses import InitVar, dataclass, field +from typing import Any, ClassVar, Dict, List, Set, Type, Union + +from ... import utils +from ..errors import ValidationError +from ..reference import Reference +from .property import Property + +ValueType = Union[str, int] + + +@dataclass +class EnumProperty(Property): + """ A property that should use an enum """ + + values: Dict[str, ValueType] + reference: Reference = field(init=False) + value_type: Type[ValueType] = field(init=False) + + title: InitVar[str] + existing_enums: InitVar[Dict[str, "EnumProperty"]] + + template: ClassVar[str] = "enum_property.pyi" + + def __post_init__(self, title: str, existing_enums: Dict[str, "EnumProperty"]) -> None: # type: ignore + reference = Reference.from_ref(title) + dedup_counter = 0 + while reference.class_name in existing_enums: + existing = existing_enums[reference.class_name] + if self.values == existing.values: + break # This is the same Enum, we're good + dedup_counter += 1 + reference = Reference.from_ref(f"{reference.class_name}{dedup_counter}") + + self.reference = reference + + for value in self.values.values(): + self.value_type = type(value) + break + + super().__post_init__() + + def get_type_string(self, no_optional: bool = False) -> str: + """ Get a string representation of type that should be used when declaring this property """ + + if no_optional or (self.required and not self.nullable): + return self.reference.class_name + return f"Optional[{self.reference.class_name}]" + + def get_imports(self, *, prefix: str) -> Set[str]: + """ + Get a set of import strings that should be included when this property is used somewhere + + Args: + prefix: A prefix to put before any relative (local) module names. This should be the number of . to get + back to the root of the generated client. + """ + imports = super().get_imports(prefix=prefix) + imports.add(f"from {prefix}models.{self.reference.module_name} import {self.reference.class_name}") + return imports + + @staticmethod + def values_from_list(values: List[ValueType]) -> Dict[str, ValueType]: + """ Convert a list of values into dict of {name: value} """ + output: Dict[str, ValueType] = {} + + for i, value in enumerate(values): + if isinstance(value, int): + if value < 0: + output[f"VALUE_NEGATIVE_{-value}"] = value + else: + output[f"VALUE_{value}"] = value + continue + if value[0].isalpha(): + key = value.upper() + else: + key = f"VALUE_{i}" + if key in output: + raise ValueError(f"Duplicate key {key} in Enum") + sanitized_key = utils.snake_case(key).upper() + output[sanitized_key] = utils.remove_string_escapes(value) + return output + + def _validate_default(self, default: Any) -> str: + inverse_values = {v: k for k, v in self.values.items()} + try: + return f"{self.reference.class_name}.{inverse_values[default]}" + except KeyError as e: + raise ValidationError() from e diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py new file mode 100644 index 000000000..a5124b78c --- /dev/null +++ b/openapi_python_client/parser/properties/model_property.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass +from typing import ClassVar, List, Set + +from ..reference import Reference +from .property import Property + + +@dataclass +class ModelProperty(Property): + """ A property which refers to another Schema """ + + reference: Reference + + required_properties: List[Property] + optional_properties: List[Property] + description: str + relative_imports: Set[str] + + template: ClassVar[str] = "ref_property.pyi" + # TODO: change to model_property.pyi + + def get_type_string(self, no_optional: bool = False) -> str: + """ Get a string representation of type that should be used when declaring this property """ + if no_optional or (self.required and not self.nullable): + return self.reference.class_name + return f"Optional[{self.reference.class_name}]" + + def get_imports(self, *, prefix: str) -> Set[str]: + """ + Get a set of import strings that should be included when this property is used somewhere + + Args: + prefix: A prefix to put before any relative (local) module names. This should be the number of . to get + back to the root of the generated client. + """ + imports = super().get_imports(prefix=prefix) + imports.update( + { + f"from {prefix}models.{self.reference.module_name} import {self.reference.class_name}", + "from typing import Dict", + "from typing import cast", + } + ) + return imports diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py new file mode 100644 index 000000000..6e5fca895 --- /dev/null +++ b/openapi_python_client/parser/properties/property.py @@ -0,0 +1,78 @@ +from dataclasses import dataclass, field +from typing import Any, ClassVar, Optional, Set + +from ... import utils +from ..errors import ValidationError + + +@dataclass +class Property: + """ + Describes a single property for a schema + + Attributes: + template: Name of the template file (if any) to use for this property. Must be stored in + templates/property_templates and must contain two macros: construct and transform. Construct will be used to + build this property from JSON data (a response from an API). Transform will be used to convert this property + to JSON data (when sending a request to the API). + + Raises: + ValidationError: Raised when the default value fails to be converted to the expected type + """ + + name: str + required: bool + nullable: bool + default: Optional[Any] + + template: ClassVar[Optional[str]] = None + _type_string: ClassVar[str] + + python_name: str = field(init=False) + + def __post_init__(self) -> None: + self.python_name = utils.to_valid_python_identifier(utils.snake_case(self.name)) + if self.default is not None: + self.default = self._validate_default(default=self.default) + + def _validate_default(self, default: Any) -> Any: + """ Check that the default value is valid for the property's type + perform any necessary sanitization """ + raise ValidationError + + def get_type_string(self, no_optional: bool = False) -> str: + """ + Get a string representation of type that should be used when declaring this property + + Args: + no_optional: Do not include Optional even if the value is optional (needed for isinstance checks) + """ + if no_optional or (self.required and not self.nullable): + return self._type_string + return f"Optional[{self._type_string}]" + + # noinspection PyUnusedLocal + def get_imports(self, *, prefix: str) -> Set[str]: + """ + Get a set of import strings that should be included when this property is used somewhere + + Args: + prefix: A prefix to put before any relative (local) module names. This should be the number of . to get + back to the root of the generated client. + """ + if self.nullable or not self.required: + return {"from typing import Optional"} + return set() + + def to_string(self) -> str: + """ How this should be declared in a dataclass """ + if self.default: + default = self.default + elif not self.required: + default = "None" + else: + default = None + + if default is not None: + return f"{self.python_name}: {self.get_type_string()} = {self.default}" + else: + return f"{self.python_name}: {self.get_type_string()}" diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py new file mode 100644 index 000000000..f6f3949f1 --- /dev/null +++ b/openapi_python_client/parser/properties/schemas.py @@ -0,0 +1,17 @@ +__all__ = ["Schemas"] + +from dataclasses import dataclass, field +from typing import Dict, List + +from ..errors import ParseError +from .enum_property import EnumProperty +from .model_property import ModelProperty + + +@dataclass +class Schemas: + """ Structure for containing all defined, shareable, and resuabled schemas (attr classes and Enums) """ + + enums: Dict[str, EnumProperty] = field(default_factory=dict) + models: Dict[str, ModelProperty] = field(default_factory=dict) + errors: List[ParseError] = field(default_factory=list) diff --git a/tests/test_parser/__init__.py b/tests/test_parser/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_parser/test_model.py b/tests/test_parser/test_model.py deleted file mode 100644 index 8322b5431..000000000 --- a/tests/test_parser/test_model.py +++ /dev/null @@ -1,82 +0,0 @@ -import openapi_python_client.schema as oai -from openapi_python_client.parser.errors import ParseError - -MODULE_NAME = "openapi_python_client.parser.model" - - -def test_model_from_data(mocker): - from openapi_python_client.parser.properties import Property - - in_data = oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - required=["RequiredEnum"], - properties={ - "RequiredEnum": mocker.MagicMock(), - "OptionalDateTime": mocker.MagicMock(), - }, - ) - required_property = mocker.MagicMock(autospec=Property) - required_imports = mocker.MagicMock() - required_property.get_imports.return_value = {required_imports} - optional_property = mocker.MagicMock(autospec=Property) - optional_imports = mocker.MagicMock() - optional_property.get_imports.return_value = {optional_imports} - property_from_data = mocker.patch( - f"{MODULE_NAME}.property_from_data", - side_effect=[required_property, optional_property], - ) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") - - from openapi_python_client.parser.model import Model, model_from_data - - result = model_from_data(data=in_data, name=mocker.MagicMock()) - - from_ref.assert_called_once_with(in_data.title) - property_from_data.assert_has_calls( - [ - mocker.call(name="RequiredEnum", required=True, data=in_data.properties["RequiredEnum"]), - mocker.call(name="OptionalDateTime", required=False, data=in_data.properties["OptionalDateTime"]), - ] - ) - required_property.get_imports.assert_called_once_with(prefix="..") - optional_property.get_imports.assert_called_once_with(prefix="..") - assert result == Model( - reference=from_ref(), - required_properties=[required_property], - optional_properties=[optional_property], - relative_imports={ - required_imports, - optional_imports, - }, - description=in_data.description, - ) - - -def test_model_from_data_property_parse_error(mocker): - in_data = oai.Schema.construct( - title=mocker.MagicMock(), - description=mocker.MagicMock(), - required=["RequiredEnum"], - properties={ - "RequiredEnum": mocker.MagicMock(), - "OptionalDateTime": mocker.MagicMock(), - }, - ) - parse_error = ParseError(data=mocker.MagicMock()) - property_from_data = mocker.patch( - f"{MODULE_NAME}.property_from_data", - return_value=parse_error, - ) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") - - from openapi_python_client.parser.model import model_from_data - - result = model_from_data(data=in_data, name=mocker.MagicMock()) - - from_ref.assert_called_once_with(in_data.title) - property_from_data.assert_called_once_with( - name="RequiredEnum", required=True, data=in_data.properties["RequiredEnum"] - ) - - assert result == parse_error diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index d0fb4a331..178ba2740 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -7,41 +7,40 @@ class TestGeneratorData: def test_from_dict(self, mocker): - Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") + build_schemas = mocker.patch(f"{MODULE_NAME}.build_schemas") EndpointCollection = mocker.patch(f"{MODULE_NAME}.EndpointCollection") + schemas = mocker.MagicMock() + endpoints_collections_by_tag = mocker.MagicMock() + EndpointCollection.from_data.return_value = (endpoints_collections_by_tag, schemas) OpenAPI = mocker.patch(f"{MODULE_NAME}.oai.OpenAPI") openapi = OpenAPI.parse_obj.return_value in_dict = mocker.MagicMock() - get_all_enums = mocker.patch(f"{MODULE_NAME}.EnumProperty.get_all_enums") from openapi_python_client.parser.openapi import GeneratorData generator_data = GeneratorData.from_dict(in_dict) OpenAPI.parse_obj.assert_called_once_with(in_dict) - Schemas.build.assert_called_once_with(schemas=openapi.components.schemas) - EndpointCollection.from_data.assert_called_once_with(data=openapi.paths) - get_all_enums.assert_called_once_with() + build_schemas.assert_called_once_with(components=openapi.components.schemas) + EndpointCollection.from_data.assert_called_once_with(data=openapi.paths, schemas=build_schemas.return_value) assert generator_data == GeneratorData( title=openapi.info.title, description=openapi.info.description, version=openapi.info.version, - endpoint_collections_by_tag=EndpointCollection.from_data.return_value, - errors=Schemas.build.return_value.errors, - models=Schemas.build.return_value.models, - enums=get_all_enums.return_value, + endpoint_collections_by_tag=endpoints_collections_by_tag, + errors=schemas.errors, + models=schemas.models, + enums=schemas.enums, ) # Test no components openapi.components = None - Schemas.build.reset_mock() + build_schemas.reset_mock() - generator_data = GeneratorData.from_dict(in_dict) + GeneratorData.from_dict(in_dict) - Schemas.build.assert_not_called() - assert generator_data.models == Schemas.return_value.models - assert generator_data.errors == Schemas.return_value.errors + build_schemas.assert_not_called() def test_from_dict_invalid_schema(self, mocker): Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") @@ -66,49 +65,6 @@ def test_from_dict_invalid_schema(self, mocker): Schemas.assert_not_called() -class TestSchemas: - def test_build(self, mocker): - from_data = mocker.patch(f"{MODULE_NAME}.model_from_data") - in_data = {"1": mocker.MagicMock(enum=None), "2": mocker.MagicMock(enum=None), "3": mocker.MagicMock(enum=None)} - schema_1 = mocker.MagicMock() - schema_2 = mocker.MagicMock() - error = ParseError() - from_data.side_effect = [schema_1, schema_2, error] - - from openapi_python_client.parser.openapi import Schemas - - result = Schemas.build(schemas=in_data) - - from_data.assert_has_calls([mocker.call(data=value, name=name) for (name, value) in in_data.items()]) - assert result == Schemas( - models={ - schema_1.reference.class_name: schema_1, - schema_2.reference.class_name: schema_2, - }, - errors=[error], - ) - - def test_build_parse_error_on_reference(self): - from openapi_python_client.parser.openapi import Schemas - - ref_schema = oai.Reference.construct() - in_data = {1: ref_schema} - result = Schemas.build(schemas=in_data) - assert result.errors[0] == ParseError(data=ref_schema, detail="Reference schemas are not supported.") - - def test_build_enums(self, mocker): - from openapi_python_client.parser.openapi import Schemas - - from_data = mocker.patch(f"{MODULE_NAME}.model_from_data") - enum_property = mocker.patch(f"{MODULE_NAME}.EnumProperty") - in_data = {"1": mocker.MagicMock(enum=["val1", "val2", "val3"])} - - Schemas.build(schemas=in_data) - - enum_property.assert_called() - from_data.assert_not_called() - - class TestEndpoint: def test_parse_request_form_body(self, mocker): ref = mocker.MagicMock() @@ -161,30 +117,32 @@ def test_parse_multipart_body_no_data(self): assert result is None def test_parse_request_json_body(self, mocker): + from openapi_python_client.parser.openapi import Endpoint, Schemas + schema = mocker.MagicMock() body = oai.RequestBody.construct( content={"application/json": oai.MediaType.construct(media_type_schema=schema)} ) property_from_data = mocker.patch(f"{MODULE_NAME}.property_from_data") + schemas = Schemas() - from openapi_python_client.parser.openapi import Endpoint - - result = Endpoint.parse_request_json_body(body) + result = Endpoint.parse_request_json_body(body=body, schemas=schemas) - property_from_data.assert_called_once_with("json_body", required=True, data=schema) - assert result == property_from_data() + property_from_data.assert_called_once_with("json_body", required=True, data=schema, schemas=schemas) + assert result == property_from_data.return_value def test_parse_request_json_body_no_data(self): - body = oai.RequestBody.construct(content={}) + from openapi_python_client.parser.openapi import Endpoint, Schemas - from openapi_python_client.parser.openapi import Endpoint + body = oai.RequestBody.construct(content={}) + schemas = Schemas() - result = Endpoint.parse_request_json_body(body) + result = Endpoint.parse_request_json_body(body=body, schemas=schemas) - assert result is None + assert result == (None, schemas) def test_add_body_no_data(self, mocker): - from openapi_python_client.parser.openapi import Endpoint + from openapi_python_client.parser.openapi import Endpoint, Schemas parse_request_form_body = mocker.patch.object(Endpoint, "parse_request_form_body") endpoint = Endpoint( @@ -196,17 +154,19 @@ def test_add_body_no_data(self, mocker): tag="tag", relative_imports={"import_3"}, ) + schemas = Schemas() - Endpoint._add_body(endpoint, oai.Operation.construct()) + Endpoint._add_body(endpoint=endpoint, data=oai.Operation.construct(), schemas=schemas) parse_request_form_body.assert_not_called() def test_add_body_bad_data(self, mocker): - from openapi_python_client.parser.openapi import Endpoint + from openapi_python_client.parser.openapi import Endpoint, Schemas mocker.patch.object(Endpoint, "parse_request_form_body") parse_error = ParseError(data=mocker.MagicMock()) - mocker.patch.object(Endpoint, "parse_request_json_body", return_value=parse_error) + other_schemas = mocker.MagicMock() + mocker.patch.object(Endpoint, "parse_request_json_body", return_value=(parse_error, other_schemas)) endpoint = Endpoint( path="path", method="method", @@ -217,13 +177,19 @@ def test_add_body_bad_data(self, mocker): relative_imports={"import_3"}, ) request_body = mocker.MagicMock() + schemas = Schemas() - result = Endpoint._add_body(endpoint, oai.Operation.construct(requestBody=request_body)) + result = Endpoint._add_body( + endpoint=endpoint, data=oai.Operation.construct(requestBody=request_body), schemas=schemas + ) - assert result == ParseError(detail=f"cannot parse body of endpoint {endpoint.name}", data=parse_error.data) + assert result == ( + ParseError(detail=f"cannot parse body of endpoint {endpoint.name}", data=parse_error.data), + other_schemas, + ) def test_add_body_happy(self, mocker): - from openapi_python_client.parser.openapi import Endpoint, Reference + from openapi_python_client.parser.openapi import Endpoint, Reference, Schemas from openapi_python_client.parser.properties import Property request_body = mocker.MagicMock() @@ -239,7 +205,10 @@ def test_add_body_happy(self, mocker): json_body = mocker.MagicMock(autospec=Property) json_body_imports = mocker.MagicMock() json_body.get_imports.return_value = {json_body_imports} - parse_request_json_body = mocker.patch.object(Endpoint, "parse_request_json_body", return_value=json_body) + parsed_schemas = mocker.MagicMock() + parse_request_json_body = mocker.patch.object( + Endpoint, "parse_request_json_body", return_value=(json_body, parsed_schemas) + ) import_string_from_reference = mocker.patch( f"{MODULE_NAME}.import_string_from_reference", side_effect=["import_1", "import_2"] ) @@ -253,11 +222,15 @@ def test_add_body_happy(self, mocker): tag="tag", relative_imports={"import_3"}, ) + initial_schemas = mocker.MagicMock() - endpoint = Endpoint._add_body(endpoint, oai.Operation.construct(requestBody=request_body)) + (endpoint, response_schemas) = Endpoint._add_body( + endpoint=endpoint, data=oai.Operation.construct(requestBody=request_body), schemas=initial_schemas + ) + assert response_schemas == parsed_schemas parse_request_form_body.assert_called_once_with(request_body) - parse_request_json_body.assert_called_once_with(request_body) + parse_request_json_body.assert_called_once_with(body=request_body, schemas=initial_schemas) parse_multipart_body.assert_called_once_with(request_body) import_string_from_reference.assert_has_calls( [ @@ -353,7 +326,7 @@ def test__add_responses(self, mocker): assert endpoint.relative_imports == {"import_1", "import_2", "import_3"} def test__add_parameters_handles_no_params(self): - from openapi_python_client.parser.openapi import Endpoint + from openapi_python_client.parser.openapi import Endpoint, Schemas endpoint = Endpoint( path="path", @@ -363,8 +336,12 @@ def test__add_parameters_handles_no_params(self): requires_security=False, tag="tag", ) + schemas = Schemas() # Just checking there's no exception here - assert Endpoint._add_parameters(endpoint, oai.Operation.construct()) == endpoint + assert Endpoint._add_parameters(endpoint=endpoint, data=oai.Operation.construct(), schemas=schemas) == ( + endpoint, + schemas, + ) def test__add_parameters_parse_error(self, mocker): from openapi_python_client.parser.openapi import Endpoint @@ -377,15 +354,22 @@ def test__add_parameters_parse_error(self, mocker): requires_security=False, tag="tag", ) + initial_schemas = mocker.MagicMock() parse_error = ParseError(data=mocker.MagicMock()) - mocker.patch(f"{MODULE_NAME}.property_from_data", return_value=parse_error) + property_schemas = mocker.MagicMock() + mocker.patch(f"{MODULE_NAME}.property_from_data", return_value=(parse_error, property_schemas)) param = oai.Parameter.construct(name="test", required=True, param_schema=mocker.MagicMock(), param_in="cookie") - result = Endpoint._add_parameters(endpoint, oai.Operation.construct(parameters=[param])) - assert result == ParseError(data=parse_error.data, detail=f"cannot parse parameter of endpoint {endpoint.name}") + result = Endpoint._add_parameters( + endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=initial_schemas + ) + assert result == ( + ParseError(data=parse_error.data, detail=f"cannot parse parameter of endpoint {endpoint.name}"), + property_schemas, + ) def test__add_parameters_fail_loudly_when_location_not_supported(self, mocker): - from openapi_python_client.parser.openapi import Endpoint + from openapi_python_client.parser.openapi import Endpoint, Schemas endpoint = Endpoint( path="path", @@ -395,11 +379,15 @@ def test__add_parameters_fail_loudly_when_location_not_supported(self, mocker): requires_security=False, tag="tag", ) - mocker.patch(f"{MODULE_NAME}.property_from_data") + parsed_schemas = mocker.MagicMock() + mocker.patch(f"{MODULE_NAME}.property_from_data", return_value=(mocker.MagicMock(), parsed_schemas)) param = oai.Parameter.construct(name="test", required=True, param_schema=mocker.MagicMock(), param_in="cookie") + schemas = Schemas() - result = Endpoint._add_parameters(endpoint, oai.Operation.construct(parameters=[param])) - assert result == ParseError(data=param, detail="Parameter must be declared in path or query") + result = Endpoint._add_parameters( + endpoint=endpoint, data=oai.Operation.construct(parameters=[param]), schemas=schemas + ) + assert result == (ParseError(data=param, detail="Parameter must be declared in path or query"), parsed_schemas) def test__add_parameters_happy(self, mocker): from openapi_python_client.parser.openapi import Endpoint @@ -423,8 +411,12 @@ def test__add_parameters_happy(self, mocker): header_prop = mocker.MagicMock(autospec=Property) header_prop_import = mocker.MagicMock() header_prop.get_imports = mocker.MagicMock(return_value={header_prop_import}) + schemas_1 = mocker.MagicMock() + schemas_2 = mocker.MagicMock() + schemas_3 = mocker.MagicMock() property_from_data = mocker.patch( - f"{MODULE_NAME}.property_from_data", side_effect=[path_prop, query_prop, header_prop] + f"{MODULE_NAME}.property_from_data", + side_effect=[(path_prop, schemas_1), (query_prop, schemas_2), (header_prop, schemas_3)], ) path_schema = mocker.MagicMock() query_schema = mocker.MagicMock() @@ -444,14 +436,15 @@ def test__add_parameters_happy(self, mocker): oai.Parameter.construct(), # Should be ignored ] ) + initial_schemas = mocker.MagicMock() - endpoint = Endpoint._add_parameters(endpoint, data) + (endpoint, schemas) = Endpoint._add_parameters(endpoint=endpoint, data=data, schemas=initial_schemas) property_from_data.assert_has_calls( [ - mocker.call(name="path_prop_name", required=True, data=path_schema), - mocker.call(name="query_prop_name", required=False, data=query_schema), - mocker.call(name="header_prop_name", required=False, data=header_schema), + mocker.call(name="path_prop_name", required=True, data=path_schema, schemas=initial_schemas), + mocker.call(name="query_prop_name", required=False, data=query_schema, schemas=schemas_1), + mocker.call(name="header_prop_name", required=False, data=header_schema, schemas=schemas_2), ] ) path_prop.get_imports.assert_called_once_with(prefix="...") @@ -461,6 +454,7 @@ def test__add_parameters_happy(self, mocker): assert endpoint.path_parameters == [path_prop] assert endpoint.query_parameters == [query_prop] assert endpoint.header_parameters == [header_prop] + assert schemas == schemas_3 def test_from_data_bad_params(self, mocker): from openapi_python_client.parser.openapi import Endpoint @@ -468,17 +462,19 @@ def test_from_data_bad_params(self, mocker): path = mocker.MagicMock() method = mocker.MagicMock() parse_error = ParseError(data=mocker.MagicMock()) - _add_parameters = mocker.patch.object(Endpoint, "_add_parameters", return_value=parse_error) + return_schemas = mocker.MagicMock() + _add_parameters = mocker.patch.object(Endpoint, "_add_parameters", return_value=(parse_error, return_schemas)) data = oai.Operation.construct( description=mocker.MagicMock(), operationId=mocker.MagicMock(), security={"blah": "bloo"}, responses=mocker.MagicMock(), ) + inital_schemas = mocker.MagicMock() - result = Endpoint.from_data(data=data, path=path, method=method, tag="default") + result = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=inital_schemas) - assert result == parse_error + assert result == (parse_error, return_schemas) def test_from_data_bad_responses(self, mocker): from openapi_python_client.parser.openapi import Endpoint @@ -486,7 +482,10 @@ def test_from_data_bad_responses(self, mocker): path = mocker.MagicMock() method = mocker.MagicMock() parse_error = ParseError(data=mocker.MagicMock()) - _add_parameters = mocker.patch.object(Endpoint, "_add_parameters") + param_schemas = mocker.MagicMock() + _add_parameters = mocker.patch.object( + Endpoint, "_add_parameters", return_value=(mocker.MagicMock(), param_schemas) + ) _add_responses = mocker.patch.object(Endpoint, "_add_responses", return_value=parse_error) data = oai.Operation.construct( description=mocker.MagicMock(), @@ -494,34 +493,40 @@ def test_from_data_bad_responses(self, mocker): security={"blah": "bloo"}, responses=mocker.MagicMock(), ) + initial_schemas = mocker.MagicMock() - result = Endpoint.from_data(data=data, path=path, method=method, tag="default") + result = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=initial_schemas) - assert result == parse_error + assert result == (parse_error, param_schemas) def test_from_data_standard(self, mocker): from openapi_python_client.parser.openapi import Endpoint path = mocker.MagicMock() method = mocker.MagicMock() - _add_parameters = mocker.patch.object(Endpoint, "_add_parameters") + param_schemas = mocker.MagicMock() + param_endpoint = mocker.MagicMock() + _add_parameters = mocker.patch.object(Endpoint, "_add_parameters", return_value=(param_endpoint, param_schemas)) _add_responses = mocker.patch.object(Endpoint, "_add_responses") - _add_body = mocker.patch.object(Endpoint, "_add_body") + body_schemas = mocker.MagicMock() + body_endpoint = mocker.MagicMock() + _add_body = mocker.patch.object(Endpoint, "_add_body", return_value=(body_endpoint, body_schemas)) data = oai.Operation.construct( description=mocker.MagicMock(), operationId=mocker.MagicMock(), security={"blah": "bloo"}, responses=mocker.MagicMock(), ) + initial_schemas = mocker.MagicMock() mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) - endpoint = Endpoint.from_data(data=data, path=path, method=method, tag="default") + endpoint = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=initial_schemas) assert endpoint == _add_body.return_value _add_parameters.assert_called_once_with( - Endpoint( + endpoint=Endpoint( path=path, method=method, description=data.description, @@ -529,34 +534,37 @@ def test_from_data_standard(self, mocker): requires_security=True, tag="default", ), - data, + data=data, + schemas=initial_schemas, ) - _add_responses.assert_called_once_with(_add_parameters.return_value, data.responses) - _add_body.assert_called_once_with(_add_responses.return_value, data) + _add_responses.assert_called_once_with(param_endpoint, data.responses) + _add_body.assert_called_once_with(endpoint=_add_responses.return_value, data=data, schemas=param_schemas) def test_from_data_no_operation_id(self, mocker): from openapi_python_client.parser.openapi import Endpoint path = "/path/with/{param}/" method = "get" - _add_parameters = mocker.patch.object(Endpoint, "_add_parameters") + _add_parameters = mocker.patch.object( + Endpoint, "_add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock()) + ) _add_responses = mocker.patch.object(Endpoint, "_add_responses") - _add_body = mocker.patch.object(Endpoint, "_add_body") + _add_body = mocker.patch.object(Endpoint, "_add_body", return_value=(mocker.MagicMock(), mocker.MagicMock())) data = oai.Operation.construct( description=mocker.MagicMock(), operationId=None, security={"blah": "bloo"}, responses=mocker.MagicMock(), ) - + schemas = mocker.MagicMock() mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) - endpoint = Endpoint.from_data(data=data, path=path, method=method, tag="default") + result = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=schemas) - assert endpoint == _add_body.return_value + assert result == _add_body.return_value _add_parameters.assert_called_once_with( - Endpoint( + endpoint=Endpoint( path=path, method=method, description=data.description, @@ -564,10 +572,13 @@ def test_from_data_no_operation_id(self, mocker): requires_security=True, tag="default", ), - data, + data=data, + schemas=schemas, + ) + _add_responses.assert_called_once_with(_add_parameters.return_value[0], data.responses) + _add_body.assert_called_once_with( + endpoint=_add_responses.return_value, data=data, schemas=_add_parameters.return_value[1] ) - _add_responses.assert_called_once_with(_add_parameters.return_value, data.responses) - _add_body.assert_called_once_with(_add_responses.return_value, data) def test_from_data_no_security(self, mocker): from openapi_python_client.parser.openapi import Endpoint @@ -578,17 +589,20 @@ def test_from_data_no_security(self, mocker): security=None, responses=mocker.MagicMock(), ) - _add_parameters = mocker.patch.object(Endpoint, "_add_parameters") + _add_parameters = mocker.patch.object( + Endpoint, "_add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock()) + ) _add_responses = mocker.patch.object(Endpoint, "_add_responses") - _add_body = mocker.patch.object(Endpoint, "_add_body") + _add_body = mocker.patch.object(Endpoint, "_add_body", return_value=(mocker.MagicMock(), mocker.MagicMock())) path = mocker.MagicMock() method = mocker.MagicMock() mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=data.description) + schemas = mocker.MagicMock() - Endpoint.from_data(data=data, path=path, method=method, tag="a") + Endpoint.from_data(data=data, path=path, method=method, tag="a", schemas=schemas) _add_parameters.assert_called_once_with( - Endpoint( + endpoint=Endpoint( path=path, method=method, description=data.description, @@ -596,10 +610,13 @@ def test_from_data_no_security(self, mocker): requires_security=False, tag="a", ), - data, + data=data, + schemas=schemas, + ) + _add_responses.assert_called_once_with(_add_parameters.return_value[0], data.responses) + _add_body.assert_called_once_with( + endpoint=_add_responses.return_value, data=data, schemas=_add_parameters.return_value[1] ) - _add_responses.assert_called_once_with(_add_parameters.return_value, data.responses) - _add_body.assert_called_once_with(_add_responses.return_value, data) class TestImportStringFromReference: @@ -637,23 +654,32 @@ def test_from_data(self, mocker): endpoint_1 = mocker.MagicMock(autospec=Endpoint, tag="default", relative_imports={"1", "2"}) endpoint_2 = mocker.MagicMock(autospec=Endpoint, tag="tag_2", relative_imports={"2"}) endpoint_3 = mocker.MagicMock(autospec=Endpoint, tag="default", relative_imports={"2", "3"}) + schemas_1 = mocker.MagicMock() + schemas_2 = mocker.MagicMock() + schemas_3 = mocker.MagicMock() endpoint_from_data = mocker.patch.object( - Endpoint, "from_data", side_effect=[endpoint_1, endpoint_2, endpoint_3] + Endpoint, + "from_data", + side_effect=[(endpoint_1, schemas_1), (endpoint_2, schemas_2), (endpoint_3, schemas_3)], ) + schemas = mocker.MagicMock() - result = EndpointCollection.from_data(data=data) + result = EndpointCollection.from_data(data=data, schemas=schemas) endpoint_from_data.assert_has_calls( [ - mocker.call(data=path_1_put, path="path_1", method="put", tag="default"), - mocker.call(data=path_1_post, path="path_1", method="post", tag="tag_2"), - mocker.call(data=path_2_get, path="path_2", method="get", tag="default"), + mocker.call(data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas), + mocker.call(data=path_1_post, path="path_1", method="post", tag="tag_2", schemas=schemas_1), + mocker.call(data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2), ], ) - assert result == { - "default": EndpointCollection("default", endpoints=[endpoint_1, endpoint_3]), - "tag_2": EndpointCollection("tag_2", endpoints=[endpoint_2]), - } + assert result == ( + { + "default": EndpointCollection("default", endpoints=[endpoint_1, endpoint_3]), + "tag_2": EndpointCollection("tag_2", endpoints=[endpoint_2]), + }, + schemas_3, + ) def test_from_data_errors(self, mocker): from openapi_python_client.parser.openapi import Endpoint, EndpointCollection, ParseError @@ -665,21 +691,30 @@ def test_from_data_errors(self, mocker): "path_1": oai.PathItem.construct(post=path_1_post, put=path_1_put), "path_2": oai.PathItem.construct(get=path_2_get), } + schemas_1 = mocker.MagicMock() + schemas_2 = mocker.MagicMock() + schemas_3 = mocker.MagicMock() endpoint_from_data = mocker.patch.object( Endpoint, "from_data", - side_effect=[ParseError(data="1"), ParseError(data="2"), mocker.MagicMock(errors=[ParseError(data="3")])], + side_effect=[ + (ParseError(data="1"), schemas_1), + (ParseError(data="2"), schemas_2), + (mocker.MagicMock(errors=[ParseError(data="3")]), schemas_3), + ], ) + schemas = mocker.MagicMock() - result = EndpointCollection.from_data(data=data) + result, result_schemas = EndpointCollection.from_data(data=data, schemas=schemas) endpoint_from_data.assert_has_calls( [ - mocker.call(data=path_1_put, path="path_1", method="put", tag="default"), - mocker.call(data=path_1_post, path="path_1", method="post", tag="tag_2"), - mocker.call(data=path_2_get, path="path_2", method="get", tag="default"), + mocker.call(data=path_1_put, path="path_1", method="put", tag="default", schemas=schemas), + mocker.call(data=path_1_post, path="path_1", method="post", tag="tag_2", schemas=schemas_1), + mocker.call(data=path_2_get, path="path_2", method="get", tag="default", schemas=schemas_2), ], ) assert result["default"].parse_errors[0].data == "1" assert result["default"].parse_errors[1].data == "3" assert result["tag_2"].parse_errors[0].data == "2" + assert result_schemas == schemas_3 diff --git a/tests/test_parser/test_properties.py b/tests/test_parser/test_properties/test_init.py similarity index 63% rename from tests/test_parser/test_properties.py rename to tests/test_parser/test_properties/test_init.py index dda80620c..7a8a0dffa 100644 --- a/tests/test_parser/test_properties.py +++ b/tests/test_parser/test_properties/test_init.py @@ -1,7 +1,7 @@ import pytest import openapi_python_client.schema as oai -from openapi_python_client.parser.errors import PropertyError, ValidationError +from openapi_python_client.parser.errors import ParseError, PropertyError, ValidationError MODULE_NAME = "openapi_python_client.parser.properties" @@ -343,42 +343,54 @@ def test___post_init__(self, mocker): from openapi_python_client.parser import properties fake_dup_enum = mocker.MagicMock() - properties._existing_enums = {"MyTestEnum": fake_dup_enum} values = {"FIRST": "first", "SECOND": "second"} enum_property = properties.EnumProperty( - name=name, required=True, default="second", values=values, title="a_title", nullable=False + name=name, + required=True, + default="second", + values=values, + title="a_title", + nullable=False, + existing_enums={"MyTestEnum": fake_dup_enum}, ) assert enum_property.default == "Deduped.SECOND" assert enum_property.python_name == name from_ref.assert_has_calls([mocker.call("a_title"), mocker.call("MyTestEnum1")]) assert enum_property.reference == deduped_reference - assert properties._existing_enums == {"MyTestEnum": fake_dup_enum, "Deduped": enum_property} # Test encountering exactly the same Enum again assert ( properties.EnumProperty( - name=name, required=True, default="second", values=values, title="a_title", nullable=False + name=name, + required=True, + default="second", + values=values, + title="a_title", + nullable=False, + existing_enums={"MyTestEnum": fake_dup_enum, "Deduped": enum_property}, ) == enum_property ) - assert properties._existing_enums == {"MyTestEnum": fake_dup_enum, "Deduped": enum_property} # What if an Enum exists with the same name, but has the same values? Don't dedupe that. fake_dup_enum.values = values from_ref.reset_mock() from_ref.side_effect = [fake_reference] enum_property = properties.EnumProperty( - name=name, required=True, default="second", values=values, title="a_title", nullable=False + name=name, + required=True, + default="second", + values=values, + title="a_title", + nullable=False, + existing_enums={"MyTestEnum": fake_dup_enum, "Deduped": enum_property}, ) assert enum_property.default == "MyTestEnum.SECOND" assert enum_property.python_name == name from_ref.assert_called_once_with("a_title") assert enum_property.reference == fake_reference - assert len(properties._existing_enums) == 2 - - properties._existing_enums = {} def test_get_type_string(self, mocker): fake_reference = mocker.MagicMock(class_name="MyTestEnum") @@ -387,13 +399,12 @@ def test_get_type_string(self, mocker): from openapi_python_client.parser import properties enum_property = properties.EnumProperty( - name="test", required=True, default=None, values={}, title="a_title", nullable=False + name="test", required=True, default=None, values={}, title="a_title", nullable=False, existing_enums={} ) assert enum_property.get_type_string() == "MyTestEnum" enum_property.required = False assert enum_property.get_type_string() == "Optional[MyTestEnum]" - properties._existing_enums = {} def test_get_imports(self, mocker): fake_reference = mocker.MagicMock(class_name="MyTestEnum", module_name="my_test_enum") @@ -403,7 +414,7 @@ def test_get_imports(self, mocker): from openapi_python_client.parser import properties enum_property = properties.EnumProperty( - name="test", required=True, default=None, values={}, title="a_title", nullable=False + name="test", required=True, default=None, values={}, title="a_title", nullable=False, existing_enums={} ) assert enum_property.get_imports(prefix=prefix) == { @@ -415,7 +426,6 @@ def test_get_imports(self, mocker): f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", "from typing import Optional", } - properties._existing_enums = {} def test_values_from_list(self): from openapi_python_client.parser.properties import EnumProperty @@ -442,20 +452,6 @@ def test_values_from_list_duplicate(self): with pytest.raises(ValueError): EnumProperty.values_from_list(data) - def test_get_all_enums(self, mocker): - from openapi_python_client.parser import properties - - properties._existing_enums = mocker.MagicMock() - assert properties.EnumProperty.get_all_enums() == properties._existing_enums - properties._existing_enums = {} - - def test_get_enum(self): - from openapi_python_client.parser import properties - - properties._existing_enums = {"test": "an enum"} - assert properties.EnumProperty.get_enum("test") == "an enum" - properties._existing_enums = {} - def test__validate_default(self, mocker): fake_reference = mocker.MagicMock(class_name="MyTestEnum", module_name="my_test_enum") mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference) @@ -463,85 +459,95 @@ def test__validate_default(self, mocker): from openapi_python_client.parser import properties enum_property = properties.EnumProperty( - name="test", required=True, default="test", values={"TEST": "test"}, title="a_title", nullable=False + name="test", + required=True, + default="test", + values={"TEST": "test"}, + title="a_title", + nullable=False, + existing_enums={}, ) assert enum_property.default == "MyTestEnum.TEST" with pytest.raises(ValidationError): properties.EnumProperty( - name="test", required=True, default="bad_val", values={"TEST": "test"}, title="a_title", nullable=False + name="test", + required=True, + default="bad_val", + values={"TEST": "test"}, + title="a_title", + nullable=False, + existing_enums={}, ) - properties._existing_enums = {} - - -class TestRefProperty: - def test_template(self, mocker): - from openapi_python_client.parser.properties import RefProperty - - ref_property = RefProperty( - name="test", - required=True, - default=None, - reference=mocker.MagicMock(class_name="MyRefClass"), - nullable=False, - ) - - assert ref_property.template == "ref_property.pyi" - - mocker.patch(f"{MODULE_NAME}.EnumProperty.get_enum", return_value="an enum") - - assert ref_property.template == "enum_property.pyi" - - def test_get_type_string(self, mocker): - from openapi_python_client.parser.properties import RefProperty - - ref_property = RefProperty( - name="test", - required=True, - default=None, - reference=mocker.MagicMock(class_name="MyRefClass"), - nullable=False, - ) - - assert ref_property.get_type_string() == "MyRefClass" - - ref_property.required = False - assert ref_property.get_type_string() == "Optional[MyRefClass]" - - def test_get_imports(self, mocker): - fake_reference = mocker.MagicMock(class_name="MyRefClass", module_name="my_test_enum") - prefix = mocker.MagicMock() - - from openapi_python_client.parser.properties import RefProperty - - p = RefProperty(name="test", required=True, default=None, reference=fake_reference, nullable=False) - - assert p.get_imports(prefix=prefix) == { - f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import Dict", - "from typing import cast", - } - - p.required = False - assert p.get_imports(prefix=prefix) == { - f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import Dict", - "from typing import cast", - "from typing import Optional", - } - - def test__validate_default(self, mocker): - from openapi_python_client.parser.properties import RefProperty - - with pytest.raises(ValidationError): - RefProperty(name="a name", required=True, default="", reference=mocker.MagicMock(), nullable=False) - enum_property = mocker.MagicMock() - enum_property._validate_default.return_value = "val1" - mocker.patch(f"{MODULE_NAME}.EnumProperty.get_enum", return_value=enum_property) - p = RefProperty(name="a name", required=True, default="", reference=mocker.MagicMock(), nullable=False) - assert p.default == "val1" +# class TestRefProperty: +# def test_template(self, mocker): +# from openapi_python_client.parser.properties import RefProperty +# +# ref_property = RefProperty( +# name="test", +# required=True, +# default=None, +# reference=mocker.MagicMock(class_name="MyRefClass"), +# nullable=False, +# ) +# +# assert ref_property.template == "ref_property.pyi" +# +# mocker.patch(f"{MODULE_NAME}.EnumProperty.get_enum", return_value="an enum") +# +# assert ref_property.template == "enum_property.pyi" +# +# def test_get_type_string(self, mocker): +# from openapi_python_client.parser.properties import RefProperty +# +# ref_property = RefProperty( +# name="test", +# required=True, +# default=None, +# reference=mocker.MagicMock(class_name="MyRefClass"), +# nullable=False, +# ) +# +# assert ref_property.get_type_string() == "MyRefClass" +# +# ref_property.required = False +# assert ref_property.get_type_string() == "Optional[MyRefClass]" +# +# def test_get_imports(self, mocker): +# fake_reference = mocker.MagicMock(class_name="MyRefClass", module_name="my_test_enum") +# prefix = mocker.MagicMock() +# +# from openapi_python_client.parser.properties import RefProperty +# +# p = RefProperty(name="test", required=True, default=None, reference=fake_reference, nullable=False) +# +# assert p.get_imports(prefix=prefix) == { +# f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", +# "from typing import Dict", +# "from typing import cast", +# } +# +# p.required = False +# assert p.get_imports(prefix=prefix) == { +# f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", +# "from typing import Dict", +# "from typing import cast", +# "from typing import Optional", +# } +# +# def test__validate_default(self, mocker): +# from openapi_python_client.parser.properties import RefProperty +# +# with pytest.raises(ValidationError): +# RefProperty(name="a name", required=True, default="", reference=mocker.MagicMock(), nullable=False) +# +# enum_property = mocker.MagicMock() +# enum_property._validate_default.return_value = "val1" +# mocker.patch(f"{MODULE_NAME}.EnumProperty.get_enum", return_value=enum_property) +# p = RefProperty(name="a name", required=True, default="", reference=mocker.MagicMock(), nullable=False) +# assert p.default == "val1" class TestDictProperty: @@ -584,9 +590,11 @@ def test_property_from_data_enum(self, mocker): EnumProperty = mocker.patch(f"{MODULE_NAME}.EnumProperty") mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) - from openapi_python_client.parser.properties import property_from_data + from openapi_python_client.parser.properties import Schemas, property_from_data - p = property_from_data(name=name, required=required, data=data) + schemas = Schemas(enums={"blah": mocker.MagicMock()}) + + prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) EnumProperty.values_from_list.assert_called_once_with(data.enum) EnumProperty.assert_called_once_with( @@ -596,56 +604,79 @@ def test_property_from_data_enum(self, mocker): default=data.default, title=name, nullable=data.nullable, + existing_enums=schemas.enums, ) - assert p == EnumProperty() + assert prop == EnumProperty.return_value + assert schemas != new_schemas, "Provided Schemas was mutated" + assert new_schemas.enums == { + "blah": new_schemas.enums["blah"], + EnumProperty.return_value.reference.class_name: EnumProperty.return_value, + } - EnumProperty.reset_mock() - data.title = mocker.MagicMock() + def test_property_from_data_ref_enum(self, mocker): + from openapi_python_client.parser.properties import Schemas, property_from_data - property_from_data( - name=name, - required=required, - data=data, - ) - EnumProperty.assert_called_once_with( - name=name, - required=required, - values=EnumProperty.values_from_list(), - default=data.default, - title=data.title, - nullable=data.nullable, - ) + name = mocker.MagicMock() + required = mocker.MagicMock() + data = oai.Reference.construct(ref=mocker.MagicMock()) + from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") + mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + enum = mocker.MagicMock() + schemas = Schemas(enums={from_ref.return_value.class_name: enum}) + + prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) + + from_ref.assert_called_once_with(data.ref) + assert prop == enum + assert schemas == new_schemas + + def test_property_from_data_ref_model(self, mocker): + from openapi_python_client.parser.properties import Schemas, property_from_data - def test_property_from_data_ref(self, mocker): name = mocker.MagicMock() required = mocker.MagicMock() data = oai.Reference.construct(ref=mocker.MagicMock()) from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") - RefProperty = mocker.patch(f"{MODULE_NAME}.RefProperty") mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + model = mocker.MagicMock() + schemas = Schemas(models={from_ref.return_value.class_name: model}) - from openapi_python_client.parser.properties import property_from_data + prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) - p = property_from_data(name=name, required=required, data=data) + from_ref.assert_called_once_with(data.ref) + assert prop == model + assert schemas == new_schemas + + def test_property_from_data_ref_not_found(self, mocker): + from openapi_python_client.parser.properties import PropertyError, Schemas, property_from_data + + name = mocker.MagicMock() + required = mocker.MagicMock() + data = oai.Reference.construct(ref=mocker.MagicMock()) + from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") + mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + schemas = Schemas() + + prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) from_ref.assert_called_once_with(data.ref) - RefProperty.assert_called_once_with( - name=name, required=required, reference=from_ref(), default=None, nullable=False - ) - assert p == RefProperty() + assert prop == PropertyError(data=data, detail="Could not find reference in parsed models or enums") + assert schemas == new_schemas def test_property_from_data_string(self, mocker): + from openapi_python_client.parser.properties import Schemas, property_from_data + _string_based_property = mocker.patch(f"{MODULE_NAME}._string_based_property") name = mocker.MagicMock() required = mocker.MagicMock() data = oai.Schema.construct(type="string") mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + schemas = Schemas() - from openapi_python_client.parser.properties import property_from_data - - p = property_from_data(name=name, required=required, data=data) + p, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) assert p == _string_based_property.return_value + assert schemas == new_schemas _string_based_property.assert_called_once_with(name=name, required=required, data=data) @pytest.mark.parametrize( @@ -654,22 +685,23 @@ def test_property_from_data_string(self, mocker): ("number", "FloatProperty"), ("integer", "IntProperty"), ("boolean", "BooleanProperty"), - ("object", "DictProperty"), ], ) def test_property_from_data_simple_types(self, mocker, openapi_type, python_type): + from openapi_python_client.parser.properties import Schemas, property_from_data + name = mocker.MagicMock() required = mocker.MagicMock() data = oai.Schema.construct(type=openapi_type) clazz = mocker.patch(f"{MODULE_NAME}.{python_type}") mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + schemas = Schemas() - from openapi_python_client.parser.properties import property_from_data - - p = property_from_data(name=name, required=required, data=data) + p, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) clazz.assert_called_once_with(name=name, required=required, default=None, nullable=False) - assert p == clazz() + assert p == clazz.return_value + assert new_schemas == schemas # Test optional values clazz.reset_mock() @@ -680,56 +712,151 @@ def test_property_from_data_simple_types(self, mocker, openapi_type, python_type name=name, required=required, data=data, + schemas=schemas, ) clazz.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable) def test_property_from_data_array(self, mocker): + from openapi_python_client.parser.properties import Schemas, property_from_data + name = mocker.MagicMock() required = mocker.MagicMock() data = oai.Schema( type="array", items={"type": "number", "default": "0.0"}, ) - ListProperty = mocker.patch(f"{MODULE_NAME}.ListProperty") - FloatProperty = mocker.patch(f"{MODULE_NAME}.FloatProperty") + build_list_property = mocker.patch(f"{MODULE_NAME}.build_list_property") mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + schemas = Schemas() - from openapi_python_client.parser.properties import property_from_data + response = property_from_data(name=name, required=required, data=data, schemas=schemas) - p = property_from_data(name=name, required=required, data=data) + assert response == build_list_property.return_value + build_list_property.assert_called_once_with(data=data, name=name, required=required, schemas=schemas) + + def test_property_from_data_union(self, mocker): + from openapi_python_client.parser.properties import Schemas, property_from_data - FloatProperty.assert_called_once_with(name=name, required=True, default="0.0", nullable=False) - ListProperty.assert_called_once_with( - name=name, required=required, default=None, inner_property=FloatProperty.return_value, nullable=False + name = mocker.MagicMock() + required = mocker.MagicMock() + data = oai.Schema.construct( + anyOf=[{"type": "number", "default": "0.0"}], + oneOf=[ + {"type": "integer", "default": "0"}, + ], ) - assert p == ListProperty.return_value + build_union_property = mocker.patch(f"{MODULE_NAME}.build_union_property") + mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + schemas = Schemas() + + response = property_from_data(name=name, required=required, data=data, schemas=schemas) - def test_property_from_data_array_no_items(self, mocker): + assert response == build_union_property.return_value + build_union_property.assert_called_once_with(data=data, name=name, required=required, schemas=schemas) + + def test_property_from_data_unsupported_type(self, mocker): name = mocker.MagicMock() required = mocker.MagicMock() - data = oai.Schema(type="array") + data = oai.Schema.construct(type=mocker.MagicMock()) - from openapi_python_client.parser.properties import property_from_data + from openapi_python_client.parser.errors import PropertyError + from openapi_python_client.parser.properties import Schemas, property_from_data - p = property_from_data(name=name, required=required, data=data) + assert property_from_data(name=name, required=required, data=data, schemas=Schemas()) == ( + PropertyError(data=data, detail=f"unknown type {data.type}"), + Schemas(), + ) + + def test_property_from_data_no_valid_props_in_data(self): + from openapi_python_client.parser.errors import PropertyError + from openapi_python_client.parser.properties import Schemas, property_from_data + + schemas = Schemas() + data = oai.Schema() + + err, new_schemas = property_from_data(name="blah", required=True, data=data, schemas=schemas) + + assert err == PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined.") + assert new_schemas == schemas + + def test_property_from_data_validation_error(self, mocker): + from openapi_python_client.parser.errors import PropertyError + from openapi_python_client.parser.properties import Schemas, property_from_data + + mocker.patch(f"{MODULE_NAME}._property_from_data").side_effect = ValidationError() + schemas = Schemas() + + data = oai.Schema() + err, new_schemas = property_from_data(name="blah", required=True, data=data, schemas=schemas) + assert err == PropertyError(detail="Failed to validate default value", data=data) + assert new_schemas == schemas + + +class TestBuildListProperty: + def test_build_list_property_no_items(self, mocker): + from openapi_python_client.parser import properties + + name = mocker.MagicMock() + required = mocker.MagicMock() + data = oai.Schema.construct(type="array") + property_from_data = mocker.patch.object(properties, "property_from_data") + schemas = properties.Schemas() + + p, new_schemas = properties.build_list_property(name=name, required=required, data=data, schemas=schemas) assert p == PropertyError(data=data, detail="type array must have items defined") + assert new_schemas == schemas + property_from_data.assert_not_called() + + def test_build_list_property_invalid_items(self, mocker): + from openapi_python_client.parser import properties - def test_property_from_data_array_invalid_items(self, mocker): name = mocker.MagicMock() required = mocker.MagicMock() data = oai.Schema( type="array", items={}, ) - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + schemas = properties.Schemas() + second_schemas = properties.Schemas(errors=["error"]) + property_from_data = mocker.patch.object( + properties, "property_from_data", return_value=(properties.PropertyError(data="blah"), second_schemas) + ) - from openapi_python_client.parser.properties import property_from_data + p, new_schemas = properties.build_list_property(name=name, required=required, data=data, schemas=schemas) - p = property_from_data(name=name, required=required, data=data) + assert p == PropertyError(data="blah", detail=f"invalid data in items of array {name}") + assert new_schemas == second_schemas + assert schemas != new_schemas, "Schema was mutated" + property_from_data.assert_called_once_with(name=f"{name}_item", required=True, data=data.items, schemas=schemas) - assert p == PropertyError(data=oai.Schema(), detail=f"invalid data in items of array {name}") + def test_build_list_property(self, mocker): + from openapi_python_client.parser import properties + name = mocker.MagicMock() + required = mocker.MagicMock() + data = oai.Schema( + type="array", + items={}, + ) + schemas = properties.Schemas() + second_schemas = properties.Schemas(errors=["error"]) + property_from_data = mocker.patch.object( + properties, "property_from_data", return_value=(mocker.MagicMock(), second_schemas) + ) + mocker.patch("openapi_python_client.utils.snake_case", return_value=name) + mocker.patch("openapi_python_client.utils.to_valid_python_identifier", return_value=name) + + p, new_schemas = properties.build_list_property(name=name, required=required, data=data, schemas=schemas) + + assert isinstance(p, properties.ListProperty) + assert p.inner_property == property_from_data.return_value[0] + assert new_schemas == second_schemas + assert schemas != new_schemas, "Schema was mutated" + property_from_data.assert_called_once_with(name=f"{name}_item", required=True, data=data.items, schemas=schemas) + + +class TestBuildUnionProperty: def test_property_from_data_union(self, mocker): name = mocker.MagicMock() required = mocker.MagicMock() @@ -771,38 +898,6 @@ def test_property_from_data_union_bad_type(self, mocker): assert p == PropertyError(detail=f"Invalid property in union {name}", data=oai.Schema()) - def test_property_from_data_unsupported_type(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Schema.construct(type=mocker.MagicMock()) - - from openapi_python_client.parser.errors import PropertyError - from openapi_python_client.parser.properties import property_from_data - - assert property_from_data(name=name, required=required, data=data) == PropertyError( - data=data, detail=f"unknown type {data.type}" - ) - - def test_property_from_data_no_valid_props_in_data(self): - from openapi_python_client.parser.errors import PropertyError - from openapi_python_client.parser.properties import property_from_data - - data = oai.Schema() - assert property_from_data(name="blah", required=True, data=data) == PropertyError( - data=data, detail="Schemas must either have one of enum, anyOf, or type defined." - ) - - def test_property_from_data_validation_error(self, mocker): - from openapi_python_client.parser.errors import PropertyError - from openapi_python_client.parser.properties import property_from_data - - mocker.patch(f"{MODULE_NAME}._property_from_data").side_effect = ValidationError() - - data = oai.Schema() - assert property_from_data(name="blah", required=True, data=data) == PropertyError( - detail="Failed to validate default value", data=data - ) - class TestStringBasedProperty: def test__string_based_property_no_format(self, mocker): @@ -935,3 +1030,130 @@ def test__string_based_property_unsupported_format(self, mocker): StringProperty.assert_called_once_with( name=name, required=required, pattern=data.pattern, default=data.default, nullable=data.nullable ) + + +def test_model_from_data(mocker): + from openapi_python_client.parser.properties import Property + + in_data = oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + required=["RequiredEnum"], + properties={ + "RequiredEnum": mocker.MagicMock(), + "OptionalDateTime": mocker.MagicMock(), + }, + ) + required_property = mocker.MagicMock(autospec=Property) + required_imports = mocker.MagicMock() + required_property.get_imports.return_value = {required_imports} + optional_property = mocker.MagicMock(autospec=Property) + optional_imports = mocker.MagicMock() + optional_property.get_imports.return_value = {optional_imports} + property_from_data = mocker.patch( + f"{MODULE_NAME}.property_from_data", + side_effect=[required_property, optional_property], + ) + from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") + + from openapi_python_client.parser.model import Model, model_from_data + + result = model_from_data(data=in_data, name=mocker.MagicMock()) + + from_ref.assert_called_once_with(in_data.title) + property_from_data.assert_has_calls( + [ + mocker.call(name="RequiredEnum", required=True, data=in_data.properties["RequiredEnum"]), + mocker.call(name="OptionalDateTime", required=False, data=in_data.properties["OptionalDateTime"]), + ] + ) + required_property.get_imports.assert_called_once_with(prefix="..") + optional_property.get_imports.assert_called_once_with(prefix="..") + assert result == Model( + reference=from_ref(), + required_properties=[required_property], + optional_properties=[optional_property], + relative_imports={ + required_imports, + optional_imports, + }, + description=in_data.description, + ) + + +def test_model_from_data_property_parse_error(mocker): + in_data = oai.Schema.construct( + title=mocker.MagicMock(), + description=mocker.MagicMock(), + required=["RequiredEnum"], + properties={ + "RequiredEnum": mocker.MagicMock(), + "OptionalDateTime": mocker.MagicMock(), + }, + ) + parse_error = ParseError(data=mocker.MagicMock()) + property_from_data = mocker.patch( + f"{MODULE_NAME}.property_from_data", + return_value=parse_error, + ) + from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") + + from openapi_python_client.parser.model import model_from_data + + result = model_from_data(data=in_data, name=mocker.MagicMock()) + + from_ref.assert_called_once_with(in_data.title) + property_from_data.assert_called_once_with( + name="RequiredEnum", required=True, data=in_data.properties["RequiredEnum"] + ) + + assert result == parse_error + + +def test_build_schemas(mocker): + build_model_property = mocker.patch(f"{MODULE_NAME}.build_model_property") + in_data = {"1": mocker.MagicMock(enum=None), "2": mocker.MagicMock(enum=None), "3": mocker.MagicMock(enum=None)} + model_1 = mocker.MagicMock() + schemas_1 = mocker.MagicMock() + model_2 = mocker.MagicMock() + schemas_2 = mocker.MagicMock() + error = PropertyError() + schemas_3 = mocker.MagicMock(errors=[]) + build_model_property.side_effect = [(model_1, schemas_1), (model_2, schemas_2), (error, schemas_3)] + + from openapi_python_client.parser.properties import Schemas, build_schemas + + result = build_schemas(components=in_data) + + build_model_property.assert_has_calls( + [ + mocker.call(data=in_data["1"], name="1", schemas=Schemas(), required=True), + mocker.call(data=in_data["2"], name="2", schemas=schemas_1, required=True), + mocker.call(data=in_data["3"], name="3", schemas=schemas_2, required=True), + ] + ) + assert result == schemas_3 + assert result.errors == [error] + + +def test_build_parse_error_on_reference(): + from openapi_python_client.parser.openapi import build_schemas + + ref_schema = oai.Reference.construct() + in_data = {"1": ref_schema} + result = build_schemas(components=in_data) + assert result.errors[0] == PropertyError(data=ref_schema, detail="Reference schemas are not supported.") + + +def test_build_enums(mocker): + from openapi_python_client.parser.openapi import build_schemas + + build_model_property = mocker.patch(f"{MODULE_NAME}.build_model_property") + schemas = mocker.MagicMock() + build_enum_property = mocker.patch(f"{MODULE_NAME}.build_enum_property", return_value=(mocker.MagicMock(), schemas)) + in_data = {"1": mocker.MagicMock(enum=["val1", "val2", "val3"])} + + build_schemas(components=in_data) + + build_enum_property.assert_called() + build_model_property.assert_not_called() From be09dad6e95ab78051bc82a4dd7a2e6bede4969c Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Mon, 2 Nov 2020 13:50:54 -0500 Subject: [PATCH 03/25] Added support for UNSET values and better differentiation between required and nullable. Model to_dict methods now have parameters to alter what fields are included/excluded --- openapi_python_client/parser/properties.py | 6 ++-- .../templates/endpoint_macros.pyi | 4 +-- openapi_python_client/templates/model.pyi | 28 +++++++++++++++++-- .../property_templates/date_property.pyi | 14 +++++++++- .../property_templates/datetime_property.pyi | 14 +++++++++- .../property_templates/dict_property.pyi | 6 ++-- .../property_templates/enum_property.pyi | 14 +++++++++- .../property_templates/file_property.pyi | 14 +++++++++- .../property_templates/list_property.pyi | 17 ++++++++++- .../property_templates/ref_property.pyi | 14 +++++++++- .../property_templates/union_property.pyi | 10 ++++++- openapi_python_client/templates/types.py | 5 +++- 12 files changed, 128 insertions(+), 18 deletions(-) diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index afc1711ec..69ab3e324 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -64,7 +64,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ if self.nullable or not self.required: - return {"from typing import Optional"} + return {"from typing import Optional", "from typing import cast", f"from {prefix}types import UNSET"} return set() def to_string(self) -> str: @@ -72,12 +72,14 @@ def to_string(self) -> str: if self.default: default = self.default elif not self.required: + default = "cast(None, UNSET)" + elif self.nullable: default = "None" else: default = None if default is not None: - return f"{self.python_name}: {self.get_type_string()} = {self.default}" + return f"{self.python_name}: {self.get_type_string()} = {default}" else: return f"{self.python_name}: {self.get_type_string()}" diff --git a/openapi_python_client/templates/endpoint_macros.pyi b/openapi_python_client/templates/endpoint_macros.pyi index 7fe2ca053..fcfad4981 100644 --- a/openapi_python_client/templates/endpoint_macros.pyi +++ b/openapi_python_client/templates/endpoint_macros.pyi @@ -4,7 +4,7 @@ {% if parameter.required %} headers["{{ parameter.python_name | kebabcase}}"] = {{ parameter.python_name }} {% else %} -if {{ parameter.python_name }} is not None: +if {{ parameter.python_name }} is not UNSET: headers["{{ parameter.python_name | kebabcase}}"] = {{ parameter.python_name }} {% endif %} {% endfor %} @@ -33,7 +33,7 @@ params: Dict[str, Any] = { } {% for property in endpoint.query_parameters %} {% if not property.required %} -if {{ property.python_name }} is not None: +if {{ property.python_name }} is not UNSET: {% if property.template %} params["{{ property.name }}"] = {{ "json_" + property.python_name }} {% else %} diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index 899256b2d..5c912f988 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -1,7 +1,9 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional, Set import attr +from ..types import UNSET + {% for relative in model.relative_imports %} {{ relative }} {% endfor %} @@ -14,7 +16,13 @@ class {{ model.reference.class_name }}: {{ property.to_string() }} {% endfor %} - def to_dict(self) -> Dict[str, Any]: + def to_dict( + self, + include: Optional[Set[str]], + exclude: Optional[Set[str]], + exclude_unset: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: {% for property in model.required_properties + model.optional_properties %} {% if property.template %} {% from "property_templates/" + property.template import transform %} @@ -24,12 +32,26 @@ class {{ model.reference.class_name }}: {% endif %} {% endfor %} - return { + all_properties = { {% for property in model.required_properties + model.optional_properties %} "{{ property.name }}": {{ property.python_name }}, {% endfor %} } + trimmed_properties: Dict[str, Any] = {} + for property_name, property_value in all_properties.items(): + if include is not None and property_name not in include: + continue + if exclude is not None and property_name in exclude: + continue + if exclude_unset and property_value is UNSET: + continue + if exclude_none and property_value is None: + continue + trimmed_properties[property_name] = property_value + + return trimmed_properties + @staticmethod def from_dict(d: Dict[str, Any]) -> "{{ model.reference.class_name }}": {% for property in model.required_properties + model.optional_properties %} diff --git a/openapi_python_client/templates/property_templates/date_property.pyi b/openapi_python_client/templates/property_templates/date_property.pyi index 416acc1e1..66d7afe7a 100644 --- a/openapi_python_client/templates/property_templates/date_property.pyi +++ b/openapi_python_client/templates/property_templates/date_property.pyi @@ -10,8 +10,20 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} +{% if property.nullable %} +{{ destination }} = {{ source }}.isoformat() if {{ source }} else None +{% else %} {{ destination }} = {{ source }}.isoformat() +{% endif %} {% else %} -{{ destination }} = {{ source }}.isoformat() if {{ source }} else None +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +else: + {{ destination }} = {{ source }}.isoformat() if {{ source }} else None +{% else %} +else: + {{ destination }} = {{ source }}.isoformat() +{% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/datetime_property.pyi b/openapi_python_client/templates/property_templates/datetime_property.pyi index ff57249a5..534327972 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.pyi +++ b/openapi_python_client/templates/property_templates/datetime_property.pyi @@ -10,8 +10,20 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} +{% if property.nullable %} +{{ destination }} = {{ source }}.isoformat() if {{ source }} else None +{% else %} {{ destination }} = {{ source }}.isoformat() +{% endif %} {% else %} -{{ destination }} = {{ source }}.isoformat() if {{ source }} else None +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +else: + {{ destination }} = {{ source }}.isoformat() if {{ source }} else None +{% else %} +else: + {{ destination }} = {{ source }}.isoformat() +{% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/dict_property.pyi b/openapi_python_client/templates/property_templates/dict_property.pyi index 2feabd1d2..62440ebeb 100644 --- a/openapi_python_client/templates/property_templates/dict_property.pyi +++ b/openapi_python_client/templates/property_templates/dict_property.pyi @@ -9,9 +9,9 @@ if {{ source }} is not None: {% endmacro %} {% macro transform(property, source, destination) %} -{% if property.required %} -{{ destination }} = {{ source }} -{% else %} +{% if property.nullable %} {{ destination }} = {{ source }} if {{ source }} else None +{% else %} +{{ destination }} = {{ source }} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/enum_property.pyi b/openapi_python_client/templates/property_templates/enum_property.pyi index 2cff340c3..3aa22fcae 100644 --- a/openapi_python_client/templates/property_templates/enum_property.pyi +++ b/openapi_python_client/templates/property_templates/enum_property.pyi @@ -10,8 +10,20 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} +{% if property.nullable %} +{{ destination }} = {{ source }}.value if {{ source }} else None +{% else %} {{ destination }} = {{ source }}.value +{% endif %} {% else %} -{{ destination }} = {{ source }}.value if {{ source }} else None +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +else: + {{ destination }} = {{ source }}.value if {{ source }} else None +{% else %} +else: + {{ destination }} = {{ source }}.value +{% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/file_property.pyi b/openapi_python_client/templates/property_templates/file_property.pyi index 444c9dbcb..667d98d81 100644 --- a/openapi_python_client/templates/property_templates/file_property.pyi +++ b/openapi_python_client/templates/property_templates/file_property.pyi @@ -5,8 +5,20 @@ {% macro transform(property, source, destination) %} {% if property.required %} +{% if property.nullable %} +{{ destination }} = {{ source }}.to_tuple() if {{ source }} else None +{% else %} {{ destination }} = {{ source }}.to_tuple() +{% endif %} {% else %} -{{ destination }} = {{ source }}.to_tuple() if {{ source }} else None +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +else: + {{ destination }} = {{ source }}.to_tuple() if {{ source }} else None +{% else %} +else: + {{ destination }} = {{ source }}.to_tuple() +{% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/list_property.pyi b/openapi_python_client/templates/property_templates/list_property.pyi index 015eb0880..e05d8b8d6 100644 --- a/openapi_python_client/templates/property_templates/list_property.pyi +++ b/openapi_python_client/templates/property_templates/list_property.pyi @@ -33,7 +33,10 @@ for {{ inner_source }} in {{ source }}: {% macro transform(property, source, destination) %} {% set inner_property = property.inner_property %} -{% if not property.required %} + + +{% if property.required %} +{% if property.nullable %} if {{ source }} is None: {{ destination }} = None else: @@ -41,4 +44,16 @@ else: {% else %} {{ _transform(property, source, destination) }} {% endif %} +{% else %} +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +elif {{ source }} is None: + {{ destination }} = None +{% endif %} +else: + {{ _transform(property, source, destination) | indent(4)}} +{% endif %} + + {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/ref_property.pyi b/openapi_python_client/templates/property_templates/ref_property.pyi index c38a5199c..90990a23e 100644 --- a/openapi_python_client/templates/property_templates/ref_property.pyi +++ b/openapi_python_client/templates/property_templates/ref_property.pyi @@ -10,8 +10,20 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} +{% if property.nullable %} +{{ destination }} = {{ source }}.to_dict() if {{ source }} else None +{% else %} {{ destination }} = {{ source }}.to_dict() +{% endif %} {% else %} -{{ destination }} = {{ source }}.to_dict() if {{ source }} else None +if {{ source }} is UNSET: + {{ destination }} = UNSET +{% if property.nullable %} +else: + {{ destination }} = {{ source }}.to_dict() if {{ source }} else None +{% else %} +else: + {{ destination }} = {{ source }}.to_dict() +{% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index 1498d68c7..de2d3012c 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -23,11 +23,19 @@ def _parse_{{ property.python_name }}(data: Dict[str, Any]) -> {{ property.get_t {% macro transform(property, source, destination) %} {% if not property.required %} +if {{ source }} is UNSET: + {{ destination }}: {{ property.get_type_string() }} = UNSET +{% endif %} +{% if property.nullable %} +{% if property.required %} if {{ source }} is None: +{% else %}{# There's an if UNSET statement before this } +elif {{ source }} is None: +{% endif %} {{ destination }}: {{ property.get_type_string() }} = None {% endif %} {% for inner_property in property.inner_properties %} - {% if loop.first and property.required %}{# No if None statement before this #} + {% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #} if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): {% elif not loop.last %} elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 951227435..84146cef2 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -1,8 +1,11 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union +from typing import BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union import attr +Unset = NewType("Unset", object) +UNSET: Unset = Unset(object()) + @attr.s(auto_attribs=True) class File: From 921f235cad97764f0b5ba66c1bb906a317b5094b Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Mon, 2 Nov 2020 17:14:36 -0500 Subject: [PATCH 04/25] Fixed typing issues in golden record and added some test endpoints --- .../api/tests/defaults_tests_defaults_post.py | 133 ++++++++++-------- .../tests/json_body_tests_json_body_post.py | 2 +- ...tional_value_tests_optional_query_param.py | 112 +++++++++++++++ .../tests/upload_file_tests_upload_post.py | 14 +- .../my_test_api_client/models/a_model.py | 81 ++++++++--- .../body_upload_file_tests_upload_post.py | 28 +++- .../models/http_validation_error.py | 38 +++-- .../models/validation_error.py | 29 +++- .../golden-record/my_test_api_client/types.py | 5 +- end_to_end_tests/openapi.json | 123 ++++++++++++++-- openapi_python_client/parser/openapi.py | 2 +- openapi_python_client/parser/properties.py | 14 +- openapi_python_client/templates/model.pyi | 6 +- .../property_templates/ref_property.pyi | 8 +- .../property_templates/union_property.pyi | 2 +- openapi_python_client/templates/types.py | 4 +- 16 files changed, 466 insertions(+), 135 deletions(-) create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 6b284cb9c..15fc274bb 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -7,33 +7,39 @@ from ...client import Client from ...models.an_enum import AnEnum from ...models.http_validation_error import HTTPValidationError -from ...types import Response +from ...types import UNSET, Response def _get_kwargs( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: str = "the default string", + datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), + date_prop: datetime.date = isoparse("1010-10-10").date(), + float_prop: float = 3.14, + int_prop: int = 7, + boolean_prop: bool = cast(bool, UNSET), + list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), + union_prop: Union[float, str] = "not a float", + enum_prop: AnEnum = cast(AnEnum, UNSET), ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - json_datetime_prop = datetime_prop.isoformat() if datetime_prop else None + if datetime_prop is UNSET: + json_datetime_prop = UNSET + else: + json_datetime_prop = datetime_prop.isoformat() - json_date_prop = date_prop.isoformat() if date_prop else None + if date_prop is UNSET: + json_date_prop = UNSET + else: + json_date_prop = date_prop.isoformat() - if list_prop is None: - json_list_prop = None + if list_prop is UNSET: + json_list_prop = UNSET else: json_list_prop = [] for list_prop_item_data in list_prop: @@ -41,33 +47,36 @@ def _get_kwargs( json_list_prop.append(list_prop_item) - if union_prop is None: - json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None + if union_prop is UNSET: + json_union_prop: Union[float, str] = UNSET elif isinstance(union_prop, float): json_union_prop = union_prop else: json_union_prop = union_prop - json_enum_prop = enum_prop.value if enum_prop else None + if enum_prop is UNSET: + json_enum_prop = UNSET + else: + json_enum_prop = enum_prop.value params: Dict[str, Any] = {} - if string_prop is not None: + if string_prop is not UNSET: params["string_prop"] = string_prop - if datetime_prop is not None: + if datetime_prop is not UNSET: params["datetime_prop"] = json_datetime_prop - if date_prop is not None: + if date_prop is not UNSET: params["date_prop"] = json_date_prop - if float_prop is not None: + if float_prop is not UNSET: params["float_prop"] = float_prop - if int_prop is not None: + if int_prop is not UNSET: params["int_prop"] = int_prop - if boolean_prop is not None: + if boolean_prop is not UNSET: params["boolean_prop"] = boolean_prop - if list_prop is not None: + if list_prop is not UNSET: params["list_prop"] = json_list_prop - if union_prop is not None: + if union_prop is not UNSET: params["union_prop"] = json_union_prop - if enum_prop is not None: + if enum_prop is not UNSET: params["enum_prop"] = json_enum_prop json_json_body = json_body @@ -103,15 +112,15 @@ def sync_detailed( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: str = "the default string", + datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), + date_prop: datetime.date = isoparse("1010-10-10").date(), + float_prop: float = 3.14, + int_prop: int = 7, + boolean_prop: bool = cast(bool, UNSET), + list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), + union_prop: Union[float, str] = "not a float", + enum_prop: AnEnum = cast(AnEnum, UNSET), ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -138,15 +147,15 @@ def sync( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: str = "the default string", + datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), + date_prop: datetime.date = isoparse("1010-10-10").date(), + float_prop: float = 3.14, + int_prop: int = 7, + boolean_prop: bool = cast(bool, UNSET), + list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), + union_prop: Union[float, str] = "not a float", + enum_prop: AnEnum = cast(AnEnum, UNSET), ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -169,15 +178,15 @@ async def asyncio_detailed( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: str = "the default string", + datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), + date_prop: datetime.date = isoparse("1010-10-10").date(), + float_prop: float = 3.14, + int_prop: int = 7, + boolean_prop: bool = cast(bool, UNSET), + list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), + union_prop: Union[float, str] = "not a float", + enum_prop: AnEnum = cast(AnEnum, UNSET), ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -203,15 +212,15 @@ async def asyncio( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: str = "the default string", + datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), + date_prop: datetime.date = isoparse("1010-10-10").date(), + float_prop: float = 3.14, + int_prop: int = 7, + boolean_prop: bool = cast(bool, UNSET), + list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), + union_prop: Union[float, str] = "not a float", + enum_prop: AnEnum = cast(AnEnum, UNSET), ) -> Optional[Union[None, HTTPValidationError]]: """ """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py index eb556c5d7..5edf4c025 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py @@ -17,7 +17,7 @@ def _get_kwargs( headers: Dict[str, Any] = client.get_headers() - json_json_body = json_body.to_dict() + json_json_body = json_body.to_dict(exclude_unset=True) return { "url": url, diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py new file mode 100644 index 000000000..91b9fb0b1 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -0,0 +1,112 @@ +from typing import Any, Dict, List, Optional, Union, cast + +import httpx + +from ...client import Client +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Response + + +def _get_kwargs( + *, + client: Client, + query_param: List[str] = cast(List[str], UNSET), +) -> Dict[str, Any]: + url = "{}/tests/optional_query_param/".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + + if query_param is UNSET: + json_query_param = UNSET + else: + json_query_param = query_param + + params: Dict[str, Any] = {} + if query_param is not UNSET: + params["query_param"] = json_query_param + + return { + "url": url, + "headers": headers, + "cookies": client.get_cookies(), + "timeout": client.get_timeout(), + "params": params, + } + + +def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: + if response.status_code == 200: + return None + if response.status_code == 422: + return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + return None + + +def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPValidationError]]: + return Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=_parse_response(response=response), + ) + + +def sync_detailed( + *, + client: Client, + query_param: List[str] = cast(List[str], UNSET), +) -> Response[Union[None, HTTPValidationError]]: + kwargs = _get_kwargs( + client=client, + query_param=query_param, + ) + + response = httpx.get( + **kwargs, + ) + + return _build_response(response=response) + + +def sync( + *, + client: Client, + query_param: List[str] = cast(List[str], UNSET), +) -> Optional[Union[None, HTTPValidationError]]: + """ Test optional query parameters """ + + return sync_detailed( + client=client, + query_param=query_param, + ).parsed + + +async def asyncio_detailed( + *, + client: Client, + query_param: List[str] = cast(List[str], UNSET), +) -> Response[Union[None, HTTPValidationError]]: + kwargs = _get_kwargs( + client=client, + query_param=query_param, + ) + + async with httpx.AsyncClient() as _client: + response = await _client.get(**kwargs) + + return _build_response(response=response) + + +async def asyncio( + *, + client: Client, + query_param: List[str] = cast(List[str], UNSET), +) -> Optional[Union[None, HTTPValidationError]]: + """ Test optional query parameters """ + + return ( + await asyncio_detailed( + client=client, + query_param=query_param, + ) + ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 9891feff6..3c228b5db 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -5,20 +5,20 @@ from ...client import Client from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError -from ...types import Response +from ...types import UNSET, Response def _get_kwargs( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: bool = cast(bool, UNSET), ) -> Dict[str, Any]: url = "{}/tests/upload".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - if keep_alive is not None: + if keep_alive is not UNSET: headers["keep-alive"] = keep_alive return { @@ -51,7 +51,7 @@ def sync_detailed( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: bool = cast(bool, UNSET), ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -70,7 +70,7 @@ def sync( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: bool = cast(bool, UNSET), ) -> Optional[Union[None, HTTPValidationError]]: """ Upload a file """ @@ -85,7 +85,7 @@ async def asyncio_detailed( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: bool = cast(bool, UNSET), ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -103,7 +103,7 @@ async def asyncio( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: bool = cast(bool, UNSET), ) -> Optional[Union[None, HTTPValidationError]]: """ Upload a file """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index a1a0ace0c..111aeffee 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -1,11 +1,12 @@ import datetime -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Set, Union, cast import attr from dateutil.parser import isoparse from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum +from ..types import UNSET @attr.s(auto_attribs=True) @@ -13,17 +14,25 @@ class AModel: """ A Model for testing all the ways custom objects can be used """ an_enum_value: AnEnum - some_dict: Optional[Dict[Any, Any]] a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date - nested_list_of_enums: Optional[List[List[DifferentEnum]]] = None - attr_1_leading_digit: Optional[str] = None - - def to_dict(self) -> Dict[str, Any]: + required_not_nullable: str + nested_list_of_enums: List[List[DifferentEnum]] = cast(List[List[DifferentEnum]], UNSET) + some_dict: Optional[Dict[Any, Any]] = None + attr_1_leading_digit: str = cast(str, UNSET) + required_nullable: Optional[str] = None + not_required_nullable: Optional[str] = cast(Optional[str], UNSET) + not_required_not_nullable: str = cast(str, UNSET) + + def to_dict( + self, + include: Optional[Set[str]] = None, + exclude: Optional[Set[str]] = None, + exclude_unset: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: an_enum_value = self.an_enum_value.value - some_dict = self.some_dict - if isinstance(self.a_camel_date_time, datetime.datetime): a_camel_date_time = self.a_camel_date_time.isoformat() @@ -32,11 +41,14 @@ def to_dict(self) -> Dict[str, Any]: a_date = self.a_date.isoformat() - if self.nested_list_of_enums is None: - nested_list_of_enums = None + required_not_nullable = self.required_not_nullable + + if self.nested_list_of_enums is UNSET: + nested_list_of_enums = UNSET else: nested_list_of_enums = [] for nested_list_of_enums_item_data in self.nested_list_of_enums: + nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value @@ -45,23 +57,44 @@ def to_dict(self) -> Dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) + some_dict = self.some_dict if self.some_dict else None + attr_1_leading_digit = self.attr_1_leading_digit + required_nullable = self.required_nullable + not_required_nullable = self.not_required_nullable + not_required_not_nullable = self.not_required_not_nullable - return { + all_properties = { "an_enum_value": an_enum_value, - "some_dict": some_dict, "aCamelDateTime": a_camel_date_time, "a_date": a_date, + "required_not_nullable": required_not_nullable, "nested_list_of_enums": nested_list_of_enums, + "some_dict": some_dict, "1_leading_digit": attr_1_leading_digit, + "required_nullable": required_nullable, + "not_required_nullable": not_required_nullable, + "not_required_not_nullable": not_required_not_nullable, } + trimmed_properties: Dict[str, Any] = {} + for property_name, property_value in all_properties.items(): + if include is not None and property_name not in include: + continue + if exclude is not None and property_name in exclude: + continue + if exclude_unset and property_value is UNSET: + continue + if exclude_none and property_value is None: + continue + trimmed_properties[property_name] = property_value + + return trimmed_properties + @staticmethod def from_dict(d: Dict[str, Any]) -> "AModel": an_enum_value = AnEnum(d["an_enum_value"]) - some_dict = d["some_dict"] - def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]: a_camel_date_time: Union[datetime.datetime, datetime.date] try: @@ -78,8 +111,10 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d a_date = isoparse(d["a_date"]).date() + required_not_nullable = d["required_not_nullable"] + nested_list_of_enums = [] - for nested_list_of_enums_item_data in d.get("nested_list_of_enums") or []: + for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) @@ -88,13 +123,25 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d nested_list_of_enums.append(nested_list_of_enums_item) - attr_1_leading_digit = d.get("1_leading_digit") + some_dict = d["some_dict"] + + attr_1_leading_digit = d.get("1_leading_digit", UNSET) + + required_nullable = d["required_nullable"] + + not_required_nullable = d.get("not_required_nullable", UNSET) + + not_required_not_nullable = d.get("not_required_not_nullable", UNSET) return AModel( an_enum_value=an_enum_value, - some_dict=some_dict, a_camel_date_time=a_camel_date_time, a_date=a_date, + required_not_nullable=required_not_nullable, nested_list_of_enums=nested_list_of_enums, + some_dict=some_dict, attr_1_leading_digit=attr_1_leading_digit, + required_nullable=required_nullable, + not_required_nullable=not_required_nullable, + not_required_not_nullable=not_required_not_nullable, ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py index 4fe7f8476..a6899351e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -1,8 +1,8 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional, Set import attr -from ..types import File +from ..types import UNSET, File @attr.s(auto_attribs=True) @@ -11,13 +11,33 @@ class BodyUploadFileTestsUploadPost: some_file: File - def to_dict(self) -> Dict[str, Any]: + def to_dict( + self, + include: Optional[Set[str]] = None, + exclude: Optional[Set[str]] = None, + exclude_unset: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: some_file = self.some_file.to_tuple() - return { + all_properties = { "some_file": some_file, } + trimmed_properties: Dict[str, Any] = {} + for property_name, property_value in all_properties.items(): + if include is not None and property_name not in include: + continue + if exclude is not None and property_name in exclude: + continue + if exclude_unset and property_value is UNSET: + continue + if exclude_none and property_value is None: + continue + trimmed_properties[property_name] = property_value + + return trimmed_properties + @staticmethod def from_dict(d: Dict[str, Any]) -> "BodyUploadFileTestsUploadPost": some_file = d["some_file"] diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py index 90cd71e8c..fe5ec3b99 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py @@ -1,34 +1,56 @@ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Set, cast import attr from ..models.validation_error import ValidationError +from ..types import UNSET @attr.s(auto_attribs=True) class HTTPValidationError: """ """ - detail: Optional[List[ValidationError]] = None + detail: List[ValidationError] = cast(List[ValidationError], UNSET) - def to_dict(self) -> Dict[str, Any]: - if self.detail is None: - detail = None + def to_dict( + self, + include: Optional[Set[str]] = None, + exclude: Optional[Set[str]] = None, + exclude_unset: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: + + if self.detail is UNSET: + detail = UNSET else: detail = [] for detail_item_data in self.detail: - detail_item = detail_item_data.to_dict() + detail_item = detail_item_data.to_dict(exclude_unset=True) detail.append(detail_item) - return { + all_properties = { "detail": detail, } + trimmed_properties: Dict[str, Any] = {} + for property_name, property_value in all_properties.items(): + if include is not None and property_name not in include: + continue + if exclude is not None and property_name in exclude: + continue + if exclude_unset and property_value is UNSET: + continue + if exclude_none and property_value is None: + continue + trimmed_properties[property_name] = property_value + + return trimmed_properties + @staticmethod def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": detail = [] - for detail_item_data in d.get("detail") or []: + for detail_item_data in d.get("detail", UNSET) or []: detail_item = ValidationError.from_dict(detail_item_data) detail.append(detail_item) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py index 1e415c476..de98717fc 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py @@ -1,7 +1,9 @@ -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional, Set import attr +from ..types import UNSET + @attr.s(auto_attribs=True) class ValidationError: @@ -11,18 +13,39 @@ class ValidationError: msg: str type: str - def to_dict(self) -> Dict[str, Any]: + def to_dict( + self, + include: Optional[Set[str]] = None, + exclude: Optional[Set[str]] = None, + exclude_unset: bool = False, + exclude_none: bool = False, + ) -> Dict[str, Any]: + loc = self.loc msg = self.msg type = self.type - return { + all_properties = { "loc": loc, "msg": msg, "type": type, } + trimmed_properties: Dict[str, Any] = {} + for property_name, property_value in all_properties.items(): + if include is not None and property_name not in include: + continue + if exclude is not None and property_name in exclude: + continue + if exclude_unset and property_value is UNSET: + continue + if exclude_none and property_value is None: + continue + trimmed_properties[property_name] = property_value + + return trimmed_properties + @staticmethod def from_dict(d: Dict[str, Any]) -> "ValidationError": loc = d["loc"] diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index 951227435..4d2d3c4f5 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record/my_test_api_client/types.py @@ -1,8 +1,11 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union +from typing import Any, BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union import attr +Unset = NewType("Unset", object) +UNSET: Any = Unset(object()) + @attr.s(auto_attribs=True) class File: diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index f7a933b42..2bde7a520 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -28,7 +28,9 @@ }, "/tests/": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Get List", "description": "Get a list of things ", "operationId": "getUserList", @@ -94,7 +96,9 @@ }, "/tests/basic_lists/strings": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Get Basic List Of Strings", "description": "Get a list of strings ", "operationId": "getBasicListOfStrings", @@ -118,7 +122,9 @@ }, "/tests/basic_lists/integers": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Get Basic List Of Integers", "description": "Get a list of integers ", "operationId": "getBasicListOfIntegers", @@ -142,7 +148,9 @@ }, "/tests/basic_lists/floats": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Get Basic List Of Floats", "description": "Get a list of floats ", "operationId": "getBasicListOfFloats", @@ -166,7 +174,9 @@ }, "/tests/basic_lists/booleans": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Get Basic List Of Booleans", "description": "Get a list of booleans ", "operationId": "getBasicListOfBooleans", @@ -190,7 +200,9 @@ }, "/tests/upload": { "post": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Upload File", "description": "Upload a file ", "operationId": "upload_file_tests_upload_post", @@ -239,7 +251,9 @@ }, "/tests/json_body": { "post": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Json Body", "description": "Try sending a JSON body ", "operationId": "json_body_tests_json_body_post", @@ -277,7 +291,9 @@ }, "/tests/defaults": { "post": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Defaults", "operationId": "defaults_tests_defaults_post", "parameters": [ @@ -351,7 +367,10 @@ "items": { "$ref": "#/components/schemas/AnEnum" }, - "default": ["FIRST_VALUE", "SECOND_VALUE"] + "default": [ + "FIRST_VALUE", + "SECOND_VALUE" + ] }, "name": "list_prop", "in": "query" @@ -422,7 +441,9 @@ }, "/tests/octet_stream": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Octet Stream", "operationId": "octet_stream_tests_octet_stream_get", "responses": { @@ -442,7 +463,9 @@ }, "/tests/no_response": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "No Response", "operationId": "no_response_tests_no_response_get", "responses": { @@ -459,7 +482,9 @@ }, "/tests/unsupported_content": { "get": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Unsupported Content", "operationId": "unsupported_content_tests_unsupported_content_get", "responses": { @@ -482,7 +507,9 @@ }, "/tests/int_enum": { "post": { - "tags": ["tests"], + "tags": [ + "tests" + ], "summary": "Int Enum", "operationId": "int_enum_tests_int_enum_post", "parameters": [ @@ -516,13 +543,61 @@ } } } + }, + "/tests/optional_query_param/": { + "get": { + "tags": [ + "tests" + ], + "summary": "Optional Query Params test", + "description": "Test optional query parameters", + "operationId": "optional_value_tests_optional_query_param", + "parameters": [ + { + "required": false, + "schema": { + "title": "Query Param", + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "string1", + "string2" + ] + }, + "name": "query_param", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } } }, "components": { "schemas": { "AModel": { "title": "AModel", - "required": ["an_enum_value", "some_dict", "aCamelDateTime", "a_date"], + "required": ["an_enum_value", "some_dict", "aCamelDateTime", "a_date", "required_nullable", "required_not_nullable"], "type": "object", "properties": { "an_enum_value": { @@ -568,6 +643,26 @@ "1_leading_digit": { "title": "Leading Digit", "type": "string" + }, + "required_nullable": { + "title": "Required AND Nullable", + "type": "string", + "nullable": true + }, + "required_not_nullable": { + "title": "Required NOT Nullable", + "type": "string", + "nullable": false + }, + "not_required_nullable": { + "title": "NOT Required AND nullable", + "type": "string", + "nullable": true + }, + "not_required_not_nullable": { + "title": "NOT Required AND NOT Nullable", + "type": "string", + "nullable": false } }, "description": "A Model for testing all the ways custom objects can be used " diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 50966f8a0..1626f1541 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -255,7 +255,7 @@ def from_data(*, data: oai.Schema, name: str) -> Union["Model", ParseError]: p = property_from_data(name=key, required=required, data=value) if isinstance(p, ParseError): return p - if required: + if p.required and not p.nullable: required_properties.append(p) else: optional_properties.append(p) diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 69ab3e324..78274e348 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -51,7 +51,7 @@ def get_type_string(self, no_optional: bool = False) -> str: Args: no_optional: Do not include Optional even if the value is optional (needed for isinstance checks) """ - if no_optional or (self.required and not self.nullable): + if no_optional or not self.nullable: return self._type_string return f"Optional[{self._type_string}]" @@ -64,7 +64,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ if self.nullable or not self.required: - return {"from typing import Optional", "from typing import cast", f"from {prefix}types import UNSET"} + return {"from typing import Any, cast, Optional", f"from {prefix}types import UNSET, Unset"} return set() def to_string(self) -> str: @@ -72,7 +72,7 @@ def to_string(self) -> str: if self.default: default = self.default elif not self.required: - default = "cast(None, UNSET)" + default = f"cast({self.get_type_string()}, UNSET)" elif self.nullable: default = "None" else: @@ -224,7 +224,7 @@ class ListProperty(Property, Generic[InnerProp]): def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or (self.required and not self.nullable): + if no_optional or not self.nullable: return f"List[{self.inner_property.get_type_string()}]" return f"Optional[List[{self.inner_property.get_type_string()}]]" @@ -256,7 +256,7 @@ def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ inner_types = [p.get_type_string() for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) - if no_optional or (self.required and not self.nullable): + if no_optional or not self.nullable: return f"Union[{inner_prop_string}]" return f"Optional[Union[{inner_prop_string}]]" @@ -331,7 +331,7 @@ def get_enum(name: str) -> Optional["EnumProperty"]: def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or (self.required and not self.nullable): + if no_optional or not self.nullable: return self.reference.class_name return f"Optional[{self.reference.class_name}]" @@ -392,7 +392,7 @@ def template(self) -> str: # type: ignore def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or (self.required and not self.nullable): + if no_optional or not self.nullable: return self.reference.class_name return f"Optional[{self.reference.class_name}]" diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index 5c912f988..8583bb3f0 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -18,8 +18,8 @@ class {{ model.reference.class_name }}: def to_dict( self, - include: Optional[Set[str]], - exclude: Optional[Set[str]], + include: Optional[Set[str]] = None, + exclude: Optional[Set[str]] = None, exclude_unset: bool = False, exclude_none: bool = False, ) -> Dict[str, Any]: @@ -58,7 +58,7 @@ class {{ model.reference.class_name }}: {% if property.required %} {% set property_source = 'd["' + property.name + '"]' %} {% else %} - {% set property_source = 'd.get("' + property.name + '")' %} + {% set property_source = 'd.get("' + property.name + '", UNSET)' %} {% endif %} {% if property.template %} {% from "property_templates/" + property.template import construct %} diff --git a/openapi_python_client/templates/property_templates/ref_property.pyi b/openapi_python_client/templates/property_templates/ref_property.pyi index 90990a23e..b43c8ea71 100644 --- a/openapi_python_client/templates/property_templates/ref_property.pyi +++ b/openapi_python_client/templates/property_templates/ref_property.pyi @@ -11,19 +11,19 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} {% if property.nullable %} -{{ destination }} = {{ source }}.to_dict() if {{ source }} else None +{{ destination }} = {{ source }}.to_dict(exclude_unset=True) if {{ source }} else None {% else %} -{{ destination }} = {{ source }}.to_dict() +{{ destination }} = {{ source }}.to_dict(exclude_unset=True) {% endif %} {% else %} if {{ source }} is UNSET: {{ destination }} = UNSET {% if property.nullable %} else: - {{ destination }} = {{ source }}.to_dict() if {{ source }} else None + {{ destination }} = {{ source }}.to_dict(exclude_unset=True) if {{ source }} else None {% else %} else: - {{ destination }} = {{ source }}.to_dict() + {{ destination }} = {{ source }}.to_dict(exclude_unset=True) {% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index de2d3012c..f74604294 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -29,7 +29,7 @@ if {{ source }} is UNSET: {% if property.nullable %} {% if property.required %} if {{ source }} is None: -{% else %}{# There's an if UNSET statement before this } +{% else %}{# There's an if UNSET statement before this #} elif {{ source }} is None: {% endif %} {{ destination }}: {{ property.get_type_string() }} = None diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 84146cef2..ad022eb9c 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -1,10 +1,10 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union +from typing import BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union, Any import attr Unset = NewType("Unset", object) -UNSET: Unset = Unset(object()) +UNSET: Any = Unset(object()) @attr.s(auto_attribs=True) From ae9e2b39b923b4893605fc38d3e99e9f4c163e15 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Mon, 2 Nov 2020 17:59:57 -0500 Subject: [PATCH 05/25] Brought test coverage back to what it was before --- openapi_python_client/__init__.py | 2 +- openapi_python_client/parser/properties.py | 2 +- openapi_python_client/templates/types.py | 2 +- pyproject.toml | 2 +- tests/test_openapi_parser/test_openapi.py | 2 +- tests/test_openapi_parser/test_properties.py | 144 ++++++++++++++++--- 6 files changed, 128 insertions(+), 26 deletions(-) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 2fcca23b3..3df7594fc 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -18,7 +18,7 @@ from .utils import snake_case if sys.version_info.minor < 8: # version did not exist before 3.8, need to use a backport - from importlib_metadata import version + from importlib_metadata import version # pragma: no cover else: from importlib.metadata import version # type: ignore diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 78274e348..6fd09601f 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -64,7 +64,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ if self.nullable or not self.required: - return {"from typing import Any, cast, Optional", f"from {prefix}types import UNSET, Unset"} + return {"from typing import cast, Optional", f"from {prefix}types import UNSET, Unset"} return set() def to_string(self) -> str: diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index ad022eb9c..4d2d3c4f5 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -1,5 +1,5 @@ """ Contains some shared types for properties """ -from typing import BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union, Any +from typing import Any, BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union import attr diff --git a/pyproject.toml b/pyproject.toml index 0253d3f3b..68b1c507a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,7 @@ isort .\ && flake8 openapi_python_client\ && safety check --bare\ && mypy openapi_python_client\ - && pytest --cov openapi_python_client tests\ + && pytest --cov openapi_python_client tests --cov-report=term-missing\ """ regen = "python -m end_to_end_tests.regen_golden_record" e2e = "pytest openapi_python_client end_to_end_tests" diff --git a/tests/test_openapi_parser/test_openapi.py b/tests/test_openapi_parser/test_openapi.py index d74548e9a..987dfd763 100644 --- a/tests/test_openapi_parser/test_openapi.py +++ b/tests/test_openapi_parser/test_openapi.py @@ -80,7 +80,7 @@ def test_from_data(self, mocker): "OptionalDateTime": mocker.MagicMock(), }, ) - required_property = mocker.MagicMock(autospec=Property) + required_property = mocker.MagicMock(autospec=Property, required=True, nullable=False) required_imports = mocker.MagicMock() required_property.get_imports.return_value = {required_imports} optional_property = mocker.MagicMock(autospec=Property) diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py index dda80620c..ff8332821 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -26,7 +26,7 @@ def test_get_type_string(self): assert p.get_type_string() == "TestType" p.required = False - assert p.get_type_string() == "Optional[TestType]" + assert p.get_type_string() == "TestType" assert p.get_type_string(True) == "TestType" p.required = False @@ -41,7 +41,12 @@ def test_to_string(self, mocker): get_type_string = mocker.patch.object(p, "get_type_string") assert p.to_string() == f"{name}: {get_type_string()}" + p.required = False + assert p.to_string() == f"{name}: {get_type_string()} = cast({get_type_string()}, UNSET)" + + p.required = True + p.nullable = True assert p.to_string() == f"{name}: {get_type_string()} = None" p.default = "TEST" @@ -54,7 +59,7 @@ def test_get_imports(self): assert p.get_imports(prefix="") == set() p.required = False - assert p.get_imports(prefix="") == {"from typing import Optional"} + assert p.get_imports(prefix="") == {"from typing import cast, Optional", "from types import UNSET, Unset"} def test__validate_default(self): from openapi_python_client.parser.properties import Property @@ -77,6 +82,8 @@ def test_get_type_string(self): assert p.get_type_string() == "str" p.required = False + assert p.get_type_string() == "str" + p.nullable = True assert p.get_type_string() == "Optional[str]" def test__validate_default(self): @@ -99,10 +106,20 @@ def test_get_imports(self): p.required = False assert p.get_imports(prefix="...") == { - "from typing import Optional", "import datetime", "from typing import cast", "from dateutil.parser import isoparse", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix="...") == { + "import datetime", + "from typing import cast", + "from dateutil.parser import isoparse", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): @@ -127,11 +144,21 @@ def test_get_imports(self): } p.required = False - assert p.get_imports(prefix="..") == { - "from typing import Optional", + assert p.get_imports(prefix="...") == { "import datetime", "from typing import cast", "from dateutil.parser import isoparse", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix="...") == { + "import datetime", + "from typing import cast", + "from dateutil.parser import isoparse", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): @@ -148,14 +175,24 @@ class TestFileProperty: def test_get_imports(self): from openapi_python_client.parser.properties import FileProperty - prefix = ".." + prefix = "..." p = FileProperty(name="test", required=True, default=None, nullable=False) - assert p.get_imports(prefix=prefix) == {"from ..types import File"} + assert p.get_imports(prefix=prefix) == { + "from ...types import File", + } p.required = False assert p.get_imports(prefix=prefix) == { - "from typing import Optional", - "from ..types import File", + "from ...types import File", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix=prefix) == { + "from ...types import File", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): @@ -217,7 +254,11 @@ def test_get_type_string(self, mocker): p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=False) assert p.get_type_string() == f"List[{inner_type_string}]" + p.required = False + assert p.get_type_string() == f"List[{inner_type_string}]" + + p.nullable = True assert p.get_type_string() == f"Optional[List[{inner_type_string}]]" def test_get_type_imports(self, mocker): @@ -226,18 +267,28 @@ def test_get_type_imports(self, mocker): inner_property = mocker.MagicMock() inner_import = mocker.MagicMock() inner_property.get_imports.return_value = {inner_import} - prefix = mocker.MagicMock() + prefix = "..." p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=False) assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", } + p.required = False assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", - "from typing import Optional", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix=prefix) == { + inner_import, + "from typing import List", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self, mocker): @@ -266,7 +317,11 @@ def test_get_type_string(self, mocker): ) assert p.get_type_string() == "Union[inner_type_string_1, inner_type_string_2]" + p.required = False + assert p.get_type_string() == "Union[inner_type_string_1, inner_type_string_2]" + + p.nullable = True assert p.get_type_string() == "Optional[Union[inner_type_string_1, inner_type_string_2]]" def test_get_type_imports(self, mocker): @@ -278,7 +333,7 @@ def test_get_type_imports(self, mocker): inner_property_2 = mocker.MagicMock() inner_import_2 = mocker.MagicMock() inner_property_2.get_imports.return_value = {inner_import_2} - prefix = mocker.MagicMock() + prefix = "..." p = UnionProperty( name="test", required=True, @@ -292,12 +347,23 @@ def test_get_type_imports(self, mocker): inner_import_2, "from typing import Union", } + p.required = False assert p.get_imports(prefix=prefix) == { inner_import_1, inner_import_2, "from typing import Union", - "from typing import Optional", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix=prefix) == { + inner_import_1, + inner_import_2, + "from typing import Union", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self, mocker): @@ -391,14 +457,19 @@ def test_get_type_string(self, mocker): ) assert enum_property.get_type_string() == "MyTestEnum" + enum_property.required = False + assert enum_property.get_type_string() == "MyTestEnum" + + enum_property.nullable = True assert enum_property.get_type_string() == "Optional[MyTestEnum]" + properties._existing_enums = {} def test_get_imports(self, mocker): fake_reference = mocker.MagicMock(class_name="MyTestEnum", module_name="my_test_enum") mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference) - prefix = mocker.MagicMock() + prefix = "..." from openapi_python_client.parser import properties @@ -407,14 +478,23 @@ def test_get_imports(self, mocker): ) assert enum_property.get_imports(prefix=prefix) == { - f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}" + f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", } enum_property.required = False assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import Optional", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } + + enum_property.nullable = True + assert enum_property.get_imports(prefix=prefix) == { + f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + properties._existing_enums = {} def test_values_from_list(self): @@ -507,11 +587,14 @@ def test_get_type_string(self, mocker): assert ref_property.get_type_string() == "MyRefClass" ref_property.required = False + assert ref_property.get_type_string() == "MyRefClass" + + ref_property.nullable = True assert ref_property.get_type_string() == "Optional[MyRefClass]" def test_get_imports(self, mocker): fake_reference = mocker.MagicMock(class_name="MyRefClass", module_name="my_test_enum") - prefix = mocker.MagicMock() + prefix = "..." from openapi_python_client.parser.properties import RefProperty @@ -528,7 +611,17 @@ def test_get_imports(self, mocker): f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", "from typing import Dict", "from typing import cast", - "from typing import Optional", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = True + assert p.get_imports(prefix=prefix) == { + f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", + "from typing import Dict", + "from typing import cast", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self, mocker): @@ -548,7 +641,7 @@ class TestDictProperty: def test_get_imports(self, mocker): from openapi_python_client.parser.properties import DictProperty - prefix = mocker.MagicMock() + prefix = "..." p = DictProperty(name="test", required=True, default=None, nullable=False) assert p.get_imports(prefix=prefix) == { "from typing import Dict", @@ -556,16 +649,25 @@ def test_get_imports(self, mocker): p.required = False assert p.get_imports(prefix=prefix) == { - "from typing import Optional", "from typing import Dict", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", + } + + p.nullable = False + assert p.get_imports(prefix=prefix) == { + "from typing import Dict", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } p.default = mocker.MagicMock() assert p.get_imports(prefix=prefix) == { - "from typing import Optional", "from typing import Dict", "from typing import cast", "from dataclasses import field", + "from typing import cast, Optional", + "from ...types import UNSET, Unset", } def test__validate_default(self): From 957ce461a31032d4a38d942ebeeec985cff2cb52 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Mon, 2 Nov 2020 18:06:50 -0500 Subject: [PATCH 06/25] Updated Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 790f686fb..d9499d687 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prefix generated identifiers to allow leading digits in field names (#206 - @kalzoo). ### Additions +- Better compatibility for "required" (whether or not the field must be included) and "nullable" (whether or not the field can be null) (#205 & #208). Thanks @bowenwr & @emannguitar! ## 0.6.1 - 2020-09-26 From fb4cb6f0a21af7acd7817a6a5c0b97779eb95669 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Tue, 3 Nov 2020 11:39:36 -0500 Subject: [PATCH 07/25] Apply suggestions from code review Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com> --- openapi_python_client/__init__.py | 2 +- openapi_python_client/parser/properties.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/openapi_python_client/__init__.py b/openapi_python_client/__init__.py index 3df7594fc..2fcca23b3 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -18,7 +18,7 @@ from .utils import snake_case if sys.version_info.minor < 8: # version did not exist before 3.8, need to use a backport - from importlib_metadata import version # pragma: no cover + from importlib_metadata import version else: from importlib.metadata import version # type: ignore diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 6fd09601f..adf3083fa 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -64,7 +64,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ if self.nullable or not self.required: - return {"from typing import cast, Optional", f"from {prefix}types import UNSET, Unset"} + return {"from typing import Union, Optional", f"from {prefix}types import UNSET, Unset"} return set() def to_string(self) -> str: @@ -72,7 +72,7 @@ def to_string(self) -> str: if self.default: default = self.default elif not self.required: - default = f"cast({self.get_type_string()}, UNSET)" + default = "UNSET" elif self.nullable: default = "None" else: @@ -224,9 +224,12 @@ class ListProperty(Property, Generic[InnerProp]): def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or not self.nullable: - return f"List[{self.inner_property.get_type_string()}]" - return f"Optional[List[{self.inner_property.get_type_string()}]]" + type_string = f"List[{self.inner_property.get_type_string()}]" + if not no_optional and self.nullable: + type_string = f"Optional[{type_string}]" + if not self.required: + type_string = f"Union[Unset, {type_string}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ From 8f7d51b21621b75cc03c5a8e5282915655810d0a Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Tue, 3 Nov 2020 12:37:44 -0500 Subject: [PATCH 08/25] Removed to_dict params and cleaned up type strings --- .../api/tests/defaults_tests_defaults_post.py | 94 +++++++++---------- .../tests/json_body_tests_json_body_post.py | 2 +- ...tional_value_tests_optional_query_param.py | 12 +-- .../tests/upload_file_tests_upload_post.py | 12 +-- .../my_test_api_client/models/a_model.py | 52 ++++------ .../body_upload_file_tests_upload_post.py | 28 +----- .../models/http_validation_error.py | 38 ++------ .../models/validation_error.py | 28 +----- .../golden-record/my_test_api_client/types.py | 10 +- openapi_python_client/parser/properties.py | 49 ++++++---- openapi_python_client/templates/model.pyi | 35 +++---- .../property_templates/ref_property.pyi | 8 +- .../property_templates/union_property.pyi | 4 +- openapi_python_client/templates/types.py | 5 +- 14 files changed, 157 insertions(+), 220 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 15fc274bb..effc7b57d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -7,22 +7,22 @@ from ...client import Client from ...models.an_enum import AnEnum from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, client: Client, json_body: Dict[Any, Any], - string_prop: str = "the default string", - datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), - date_prop: datetime.date = isoparse("1010-10-10").date(), - float_prop: float = 3.14, - int_prop: int = 7, - boolean_prop: bool = cast(bool, UNSET), - list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), - union_prop: Union[float, str] = "not a float", - enum_prop: AnEnum = cast(AnEnum, UNSET), + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) @@ -48,7 +48,7 @@ def _get_kwargs( json_list_prop.append(list_prop_item) if union_prop is UNSET: - json_union_prop: Union[float, str] = UNSET + json_union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = UNSET elif isinstance(union_prop, float): json_union_prop = union_prop else: @@ -112,15 +112,15 @@ def sync_detailed( *, client: Client, json_body: Dict[Any, Any], - string_prop: str = "the default string", - datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), - date_prop: datetime.date = isoparse("1010-10-10").date(), - float_prop: float = 3.14, - int_prop: int = 7, - boolean_prop: bool = cast(bool, UNSET), - list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), - union_prop: Union[float, str] = "not a float", - enum_prop: AnEnum = cast(AnEnum, UNSET), + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -147,15 +147,15 @@ def sync( *, client: Client, json_body: Dict[Any, Any], - string_prop: str = "the default string", - datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), - date_prop: datetime.date = isoparse("1010-10-10").date(), - float_prop: float = 3.14, - int_prop: int = 7, - boolean_prop: bool = cast(bool, UNSET), - list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), - union_prop: Union[float, str] = "not a float", - enum_prop: AnEnum = cast(AnEnum, UNSET), + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -178,15 +178,15 @@ async def asyncio_detailed( *, client: Client, json_body: Dict[Any, Any], - string_prop: str = "the default string", - datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), - date_prop: datetime.date = isoparse("1010-10-10").date(), - float_prop: float = 3.14, - int_prop: int = 7, - boolean_prop: bool = cast(bool, UNSET), - list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), - union_prop: Union[float, str] = "not a float", - enum_prop: AnEnum = cast(AnEnum, UNSET), + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -212,15 +212,15 @@ async def asyncio( *, client: Client, json_body: Dict[Any, Any], - string_prop: str = "the default string", - datetime_prop: datetime.datetime = isoparse("1010-10-10T00:00:00"), - date_prop: datetime.date = isoparse("1010-10-10").date(), - float_prop: float = 3.14, - int_prop: int = 7, - boolean_prop: bool = cast(bool, UNSET), - list_prop: List[AnEnum] = cast(List[AnEnum], UNSET), - union_prop: Union[float, str] = "not a float", - enum_prop: AnEnum = cast(AnEnum, UNSET), + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py index 5edf4c025..eb556c5d7 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py @@ -17,7 +17,7 @@ def _get_kwargs( headers: Dict[str, Any] = client.get_headers() - json_json_body = json_body.to_dict(exclude_unset=True) + json_json_body = json_body.to_dict() return { "url": url, diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py index 91b9fb0b1..10c9d8d3a 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -4,13 +4,13 @@ from ...client import Client from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, client: Client, - query_param: List[str] = cast(List[str], UNSET), + query_param: Union[Unset, List[str]] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/optional_query_param/".format(client.base_url) @@ -54,7 +54,7 @@ def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPVal def sync_detailed( *, client: Client, - query_param: List[str] = cast(List[str], UNSET), + query_param: Union[Unset, List[str]] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -71,7 +71,7 @@ def sync_detailed( def sync( *, client: Client, - query_param: List[str] = cast(List[str], UNSET), + query_param: Union[Unset, List[str]] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ Test optional query parameters """ @@ -84,7 +84,7 @@ def sync( async def asyncio_detailed( *, client: Client, - query_param: List[str] = cast(List[str], UNSET), + query_param: Union[Unset, List[str]] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -100,7 +100,7 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - query_param: List[str] = cast(List[str], UNSET), + query_param: Union[Unset, List[str]] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ Test optional query parameters """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 3c228b5db..d0b31d9f7 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -5,14 +5,14 @@ from ...client import Client from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError -from ...types import UNSET, Response +from ...types import UNSET, Response, Unset def _get_kwargs( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: bool = cast(bool, UNSET), + keep_alive: Union[Unset, bool] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/upload".format(client.base_url) @@ -51,7 +51,7 @@ def sync_detailed( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: bool = cast(bool, UNSET), + keep_alive: Union[Unset, bool] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -70,7 +70,7 @@ def sync( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: bool = cast(bool, UNSET), + keep_alive: Union[Unset, bool] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ Upload a file """ @@ -85,7 +85,7 @@ async def asyncio_detailed( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: bool = cast(bool, UNSET), + keep_alive: Union[Unset, bool] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -103,7 +103,7 @@ async def asyncio( *, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: bool = cast(bool, UNSET), + keep_alive: Union[Unset, bool] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ Upload a file """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index 111aeffee..39f6dac0d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -1,12 +1,12 @@ import datetime -from typing import Any, Dict, List, Optional, Set, Union, cast +from typing import Any, Dict, List, Optional, Union import attr from dateutil.parser import isoparse from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum -from ..types import UNSET +from ..types import UNSET, Unset @attr.s(auto_attribs=True) @@ -17,20 +17,14 @@ class AModel: a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date required_not_nullable: str - nested_list_of_enums: List[List[DifferentEnum]] = cast(List[List[DifferentEnum]], UNSET) + nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET some_dict: Optional[Dict[Any, Any]] = None - attr_1_leading_digit: str = cast(str, UNSET) + attr_1_leading_digit: Union[Unset, str] = UNSET required_nullable: Optional[str] = None - not_required_nullable: Optional[str] = cast(Optional[str], UNSET) - not_required_not_nullable: str = cast(str, UNSET) - - def to_dict( - self, - include: Optional[Set[str]] = None, - exclude: Optional[Set[str]] = None, - exclude_unset: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: + not_required_nullable: Union[Unset, Optional[str]] = UNSET + not_required_not_nullable: Union[Unset, str] = UNSET + + def to_dict(self) -> Dict[str, Any]: an_enum_value = self.an_enum_value.value if isinstance(self.a_camel_date_time, datetime.datetime): @@ -64,32 +58,24 @@ def to_dict( not_required_nullable = self.not_required_nullable not_required_not_nullable = self.not_required_not_nullable - all_properties = { + field_dict = { "an_enum_value": an_enum_value, "aCamelDateTime": a_camel_date_time, "a_date": a_date, "required_not_nullable": required_not_nullable, - "nested_list_of_enums": nested_list_of_enums, "some_dict": some_dict, - "1_leading_digit": attr_1_leading_digit, "required_nullable": required_nullable, - "not_required_nullable": not_required_nullable, - "not_required_not_nullable": not_required_not_nullable, } - - trimmed_properties: Dict[str, Any] = {} - for property_name, property_value in all_properties.items(): - if include is not None and property_name not in include: - continue - if exclude is not None and property_name in exclude: - continue - if exclude_unset and property_value is UNSET: - continue - if exclude_none and property_value is None: - continue - trimmed_properties[property_name] = property_value - - return trimmed_properties + if nested_list_of_enums is not UNSET: + field_dict["nested_list_of_enums"] = nested_list_of_enums + if attr_1_leading_digit is not UNSET: + field_dict["1_leading_digit"] = attr_1_leading_digit + if not_required_nullable is not UNSET: + field_dict["not_required_nullable"] = not_required_nullable + if not_required_not_nullable is not UNSET: + field_dict["not_required_not_nullable"] = not_required_not_nullable + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "AModel": diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py index a6899351e..3435bd290 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -1,8 +1,8 @@ -from typing import Any, Dict, Optional, Set +from typing import Any, Dict import attr -from ..types import UNSET, File +from ..types import File @attr.s(auto_attribs=True) @@ -11,32 +11,14 @@ class BodyUploadFileTestsUploadPost: some_file: File - def to_dict( - self, - include: Optional[Set[str]] = None, - exclude: Optional[Set[str]] = None, - exclude_unset: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: + def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() - all_properties = { + field_dict = { "some_file": some_file, } - trimmed_properties: Dict[str, Any] = {} - for property_name, property_value in all_properties.items(): - if include is not None and property_name not in include: - continue - if exclude is not None and property_name in exclude: - continue - if exclude_unset and property_value is UNSET: - continue - if exclude_none and property_value is None: - continue - trimmed_properties[property_name] = property_value - - return trimmed_properties + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "BodyUploadFileTestsUploadPost": diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py index fe5ec3b99..aa0d796ed 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py @@ -1,51 +1,33 @@ -from typing import Any, Dict, List, Optional, Set, cast +from typing import Any, Dict, List, Union import attr from ..models.validation_error import ValidationError -from ..types import UNSET +from ..types import UNSET, Unset @attr.s(auto_attribs=True) class HTTPValidationError: """ """ - detail: List[ValidationError] = cast(List[ValidationError], UNSET) + detail: Union[Unset, List[ValidationError]] = UNSET - def to_dict( - self, - include: Optional[Set[str]] = None, - exclude: Optional[Set[str]] = None, - exclude_unset: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: + def to_dict(self) -> Dict[str, Any]: if self.detail is UNSET: detail = UNSET else: detail = [] for detail_item_data in self.detail: - detail_item = detail_item_data.to_dict(exclude_unset=True) + detail_item = detail_item_data.to_dict() detail.append(detail_item) - all_properties = { - "detail": detail, - } - - trimmed_properties: Dict[str, Any] = {} - for property_name, property_value in all_properties.items(): - if include is not None and property_name not in include: - continue - if exclude is not None and property_name in exclude: - continue - if exclude_unset and property_value is UNSET: - continue - if exclude_none and property_value is None: - continue - trimmed_properties[property_name] = property_value - - return trimmed_properties + field_dict = {} + if detail is not UNSET: + field_dict["detail"] = detail + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py index de98717fc..9f5fe0fe0 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py @@ -1,9 +1,7 @@ -from typing import Any, Dict, List, Optional, Set +from typing import Any, Dict, List import attr -from ..types import UNSET - @attr.s(auto_attribs=True) class ValidationError: @@ -13,38 +11,20 @@ class ValidationError: msg: str type: str - def to_dict( - self, - include: Optional[Set[str]] = None, - exclude: Optional[Set[str]] = None, - exclude_unset: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: + def to_dict(self) -> Dict[str, Any]: loc = self.loc msg = self.msg type = self.type - all_properties = { + field_dict = { "loc": loc, "msg": msg, "type": type, } - trimmed_properties: Dict[str, Any] = {} - for property_name, property_value in all_properties.items(): - if include is not None and property_name not in include: - continue - if exclude is not None and property_name in exclude: - continue - if exclude_unset and property_value is UNSET: - continue - if exclude_none and property_value is None: - continue - trimmed_properties[property_name] = property_value - - return trimmed_properties + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "ValidationError": diff --git a/end_to_end_tests/golden-record/my_test_api_client/types.py b/end_to_end_tests/golden-record/my_test_api_client/types.py index 4d2d3c4f5..7f0f544d8 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record/my_test_api_client/types.py @@ -1,10 +1,14 @@ """ Contains some shared types for properties """ -from typing import Any, BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union +from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union import attr -Unset = NewType("Unset", object) -UNSET: Any = Unset(object()) + +class Unset: + pass + + +UNSET: Unset = Unset() @attr.s(auto_attribs=True) diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index adf3083fa..80690a659 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -44,16 +44,19 @@ def _validate_default(self, default: Any) -> Any: """ Check that the default value is valid for the property's type + perform any necessary sanitization """ raise ValidationError - def get_type_string(self, no_optional: bool = False) -> str: + def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property Args: no_optional: Do not include Optional even if the value is optional (needed for isinstance checks) """ - if no_optional or not self.nullable: - return self._type_string - return f"Optional[{self._type_string}]" + type_string = self._type_string + if not no_optional and self.nullable: + type_string = f"Optional[{type_string}]" + if not no_unset and not self.required: + type_string = f"Union[Unset, {type_string}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ @@ -222,12 +225,12 @@ class ListProperty(Property, Generic[InnerProp]): inner_property: InnerProp template: ClassVar[str] = "list_property.pyi" - def get_type_string(self, no_optional: bool = False) -> str: + def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ type_string = f"List[{self.inner_property.get_type_string()}]" if not no_optional and self.nullable: type_string = f"Optional[{type_string}]" - if not self.required: + if not no_unset and not self.required: type_string = f"Union[Unset, {type_string}]" return type_string @@ -255,13 +258,16 @@ class UnionProperty(Property): inner_properties: List[Property] template: ClassVar[str] = "union_property.pyi" - def get_type_string(self, no_optional: bool = False) -> str: + def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ inner_types = [p.get_type_string() for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) - if no_optional or not self.nullable: - return f"Union[{inner_prop_string}]" - return f"Optional[Union[{inner_prop_string}]]" + type_string = f"Union[{inner_prop_string}]" + if not no_optional and self.nullable: + type_string = f"Optional[{type_string}]" + if not no_unset and not self.required: + type_string = f"Union[Unset, {type_string}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ @@ -331,12 +337,14 @@ def get_enum(name: str) -> Optional["EnumProperty"]: """ Get all the EnumProperties that have been registered keyed by class name """ return _existing_enums.get(name) - def get_type_string(self, no_optional: bool = False) -> str: + def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - - if no_optional or not self.nullable: - return self.reference.class_name - return f"Optional[{self.reference.class_name}]" + type_string = self.reference.class_name + if not no_optional and self.nullable: + type_string = f"Optional[{type_string}]" + if not no_unset and not self.required: + type_string = f"Union[Unset, {type_string}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ @@ -393,11 +401,14 @@ def template(self) -> str: # type: ignore return "enum_property.pyi" return "ref_property.pyi" - def get_type_string(self, no_optional: bool = False) -> str: + def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or not self.nullable: - return self.reference.class_name - return f"Optional[{self.reference.class_name}]" + type_string = self.reference.class_name + if not no_optional and self.nullable: + type_string = f"Optional[{type_string}]" + if not no_unset and not self.required: + type_string = f"Union[Unset, {type_string}]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index 8583bb3f0..1bfcc81f0 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -2,7 +2,7 @@ from typing import Any, Dict, Optional, Set import attr -from ..types import UNSET +from ..types import UNSET, Unset {% for relative in model.relative_imports %} {{ relative }} @@ -16,13 +16,7 @@ class {{ model.reference.class_name }}: {{ property.to_string() }} {% endfor %} - def to_dict( - self, - include: Optional[Set[str]] = None, - exclude: Optional[Set[str]] = None, - exclude_unset: bool = False, - exclude_none: bool = False, - ) -> Dict[str, Any]: + def to_dict(self) -> Dict[str, Any]: {% for property in model.required_properties + model.optional_properties %} {% if property.template %} {% from "property_templates/" + property.template import transform %} @@ -32,25 +26,22 @@ class {{ model.reference.class_name }}: {% endif %} {% endfor %} - all_properties = { + + field_dict = { {% for property in model.required_properties + model.optional_properties %} + {% if property.required %} "{{ property.name }}": {{ property.python_name }}, + {% endif %} {% endfor %} } + {% for property in model.optional_properties %} + {% if not property.required %} + if {{ property.python_name }} is not UNSET: + field_dict["{{ property.name }}"] = {{ property.python_name }} + {% endif %} + {% endfor %} - trimmed_properties: Dict[str, Any] = {} - for property_name, property_value in all_properties.items(): - if include is not None and property_name not in include: - continue - if exclude is not None and property_name in exclude: - continue - if exclude_unset and property_value is UNSET: - continue - if exclude_none and property_value is None: - continue - trimmed_properties[property_name] = property_value - - return trimmed_properties + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "{{ model.reference.class_name }}": diff --git a/openapi_python_client/templates/property_templates/ref_property.pyi b/openapi_python_client/templates/property_templates/ref_property.pyi index b43c8ea71..90990a23e 100644 --- a/openapi_python_client/templates/property_templates/ref_property.pyi +++ b/openapi_python_client/templates/property_templates/ref_property.pyi @@ -11,19 +11,19 @@ if {{ source }} is not None: {% macro transform(property, source, destination) %} {% if property.required %} {% if property.nullable %} -{{ destination }} = {{ source }}.to_dict(exclude_unset=True) if {{ source }} else None +{{ destination }} = {{ source }}.to_dict() if {{ source }} else None {% else %} -{{ destination }} = {{ source }}.to_dict(exclude_unset=True) +{{ destination }} = {{ source }}.to_dict() {% endif %} {% else %} if {{ source }} is UNSET: {{ destination }} = UNSET {% if property.nullable %} else: - {{ destination }} = {{ source }}.to_dict(exclude_unset=True) if {{ source }} else None + {{ destination }} = {{ source }}.to_dict() if {{ source }} else None {% else %} else: - {{ destination }} = {{ source }}.to_dict(exclude_unset=True) + {{ destination }} = {{ source }}.to_dict() {% endif %} {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index f74604294..710e2d1bb 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -36,9 +36,9 @@ elif {{ source }} is None: {% endif %} {% for inner_property in property.inner_properties %} {% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #} -if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): +if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True, no_unset=True) }}): {% elif not loop.last %} -elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): +elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True, no_unset=True) }}): {% else %} else: {% endif %} diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 4d2d3c4f5..99a344e09 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -3,8 +3,9 @@ import attr -Unset = NewType("Unset", object) -UNSET: Any = Unset(object()) +class Unset: + pass +UNSET: Unset = Unset() @attr.s(auto_attribs=True) From 961f6df374a9568253aa065e126dac7f718b4a58 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Wed, 4 Nov 2020 13:29:40 -0500 Subject: [PATCH 09/25] Fixed typing issues (again) --- .../api/tests/defaults_tests_defaults_post.py | 35 +++-- ...tional_value_tests_optional_query_param.py | 5 +- .../my_test_api_client/models/a_model.py | 7 +- .../models/http_validation_error.py | 6 +- .../models/validation_error.py | 1 - openapi_python_client/parser/properties.py | 4 +- .../property_templates/date_property.pyi | 6 +- .../property_templates/datetime_property.pyi | 6 +- .../property_templates/enum_property.pyi | 6 +- .../property_templates/file_property.pyi | 6 +- .../property_templates/list_property.pyi | 16 +-- .../property_templates/ref_property.pyi | 6 +- .../property_templates/union_property.pyi | 5 +- openapi_python_client/templates/types.py | 5 +- tests/test_openapi_parser/test_properties.py | 129 +++++++++++------- 15 files changed, 129 insertions(+), 114 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index effc7b57d..a251e4a9e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -21,42 +21,39 @@ def _get_kwargs( int_prop: Union[Unset, int] = 7, boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + union_prop: Union[Unset, Union[float, str]] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - if datetime_prop is UNSET: - json_datetime_prop = UNSET - else: + json_datetime_prop: Union[Unset, str] = UNSET + if not isinstance(datetime_prop, Unset): json_datetime_prop = datetime_prop.isoformat() - if date_prop is UNSET: - json_date_prop = UNSET - else: + json_date_prop: Union[Unset, str] = UNSET + if not isinstance(date_prop, Unset): json_date_prop = date_prop.isoformat() - if list_prop is UNSET: - json_list_prop = UNSET - else: + json_list_prop: Union[Unset, List[Any]] = UNSET + if not isinstance(list_prop, Unset): json_list_prop = [] for list_prop_item_data in list_prop: list_prop_item = list_prop_item_data.value json_list_prop.append(list_prop_item) - if union_prop is UNSET: - json_union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = UNSET + json_union_prop: Union[Unset, Union[float, str]] + if isinstance(union_prop, Unset): + json_union_prop = UNSET elif isinstance(union_prop, float): json_union_prop = union_prop else: json_union_prop = union_prop - if enum_prop is UNSET: - json_enum_prop = UNSET - else: + json_enum_prop: Union[Unset, AnEnum] = UNSET + if not isinstance(enum_prop, Unset): json_enum_prop = enum_prop.value params: Dict[str, Any] = {} @@ -119,7 +116,7 @@ def sync_detailed( int_prop: Union[Unset, int] = 7, boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + union_prop: Union[Unset, Union[float, str]] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( @@ -154,7 +151,7 @@ def sync( int_prop: Union[Unset, int] = 7, boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + union_prop: Union[Unset, Union[float, str]] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -185,7 +182,7 @@ async def asyncio_detailed( int_prop: Union[Unset, int] = 7, boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + union_prop: Union[Unset, Union[float, str]] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( @@ -219,7 +216,7 @@ async def asyncio( int_prop: Union[Unset, int] = 7, boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[Union[Unset, float], Union[Unset, str]]] = "not a float", + union_prop: Union[Unset, Union[float, str]] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py index 10c9d8d3a..519c543ac 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -16,9 +16,8 @@ def _get_kwargs( headers: Dict[str, Any] = client.get_headers() - if query_param is UNSET: - json_query_param = UNSET - else: + json_query_param: Union[Unset, List[Any]] = UNSET + if not isinstance(query_param, Unset): json_query_param = query_param params: Dict[str, Any] = {} diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index 39f6dac0d..e223bd393 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -36,13 +36,10 @@ def to_dict(self) -> Dict[str, Any]: a_date = self.a_date.isoformat() required_not_nullable = self.required_not_nullable - - if self.nested_list_of_enums is UNSET: - nested_list_of_enums = UNSET - else: + nested_list_of_enums: Union[Unset, List[Any]] = UNSET + if not isinstance(self.nested_list_of_enums, Unset): nested_list_of_enums = [] for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py index aa0d796ed..9d29faa4d 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py @@ -13,10 +13,8 @@ class HTTPValidationError: detail: Union[Unset, List[ValidationError]] = UNSET def to_dict(self) -> Dict[str, Any]: - - if self.detail is UNSET: - detail = UNSET - else: + detail: Union[Unset, List[Any]] = UNSET + if not isinstance(self.detail, Unset): detail = [] for detail_item_data in self.detail: detail_item = detail_item_data.to_dict() diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py index 9f5fe0fe0..77b9239ef 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py @@ -12,7 +12,6 @@ class ValidationError: type: str def to_dict(self) -> Dict[str, Any]: - loc = self.loc msg = self.msg diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 80690a659..603337f92 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -229,7 +229,7 @@ def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> """ Get a string representation of type that should be used when declaring this property """ type_string = f"List[{self.inner_property.get_type_string()}]" if not no_optional and self.nullable: - type_string = f"Optional[{type_string}]" + type_string = f"Optional[{type_string}]" if not no_unset and not self.required: type_string = f"Union[Unset, {type_string}]" return type_string @@ -260,7 +260,7 @@ class UnionProperty(Property): def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - inner_types = [p.get_type_string() for p in self.inner_properties] + inner_types = [p.get_type_string(no_unset=True) for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) type_string = f"Union[{inner_prop_string}]" if not no_optional and self.nullable: diff --git a/openapi_python_client/templates/property_templates/date_property.pyi b/openapi_python_client/templates/property_templates/date_property.pyi index 66d7afe7a..39985f1eb 100644 --- a/openapi_python_client/templates/property_templates/date_property.pyi +++ b/openapi_python_client/templates/property_templates/date_property.pyi @@ -16,13 +16,11 @@ if {{ source }} is not None: {{ destination }} = {{ source }}.isoformat() {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: Union[Unset, str] = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -else: {{ destination }} = {{ source }}.isoformat() if {{ source }} else None {% else %} -else: {{ destination }} = {{ source }}.isoformat() {% endif %} {% endif %} diff --git a/openapi_python_client/templates/property_templates/datetime_property.pyi b/openapi_python_client/templates/property_templates/datetime_property.pyi index 534327972..6eb772c54 100644 --- a/openapi_python_client/templates/property_templates/datetime_property.pyi +++ b/openapi_python_client/templates/property_templates/datetime_property.pyi @@ -16,13 +16,11 @@ if {{ source }} is not None: {{ destination }} = {{ source }}.isoformat() {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: Union[Unset, str] = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -else: {{ destination }} = {{ source }}.isoformat() if {{ source }} else None {% else %} -else: {{ destination }} = {{ source }}.isoformat() {% endif %} {% endif %} diff --git a/openapi_python_client/templates/property_templates/enum_property.pyi b/openapi_python_client/templates/property_templates/enum_property.pyi index 3aa22fcae..f5a1f6aba 100644 --- a/openapi_python_client/templates/property_templates/enum_property.pyi +++ b/openapi_python_client/templates/property_templates/enum_property.pyi @@ -16,13 +16,11 @@ if {{ source }} is not None: {{ destination }} = {{ source }}.value {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: {{ property.get_type_string() }} = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -else: {{ destination }} = {{ source }}.value if {{ source }} else None {% else %} -else: {{ destination }} = {{ source }}.value {% endif %} {% endif %} diff --git a/openapi_python_client/templates/property_templates/file_property.pyi b/openapi_python_client/templates/property_templates/file_property.pyi index 667d98d81..a66e81bd0 100644 --- a/openapi_python_client/templates/property_templates/file_property.pyi +++ b/openapi_python_client/templates/property_templates/file_property.pyi @@ -11,13 +11,11 @@ {{ destination }} = {{ source }}.to_tuple() {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: {{ property.get_type_string() }} = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -else: {{ destination }} = {{ source }}.to_tuple() if {{ source }} else None {% else %} -else: {{ destination }} = {{ source }}.to_tuple() {% endif %} {% endif %} diff --git a/openapi_python_client/templates/property_templates/list_property.pyi b/openapi_python_client/templates/property_templates/list_property.pyi index e05d8b8d6..f0ad1f0b3 100644 --- a/openapi_python_client/templates/property_templates/list_property.pyi +++ b/openapi_python_client/templates/property_templates/list_property.pyi @@ -33,8 +33,6 @@ for {{ inner_source }} in {{ source }}: {% macro transform(property, source, destination) %} {% set inner_property = property.inner_property %} - - {% if property.required %} {% if property.nullable %} if {{ source }} is None: @@ -45,15 +43,17 @@ else: {{ _transform(property, source, destination) }} {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: Union[Unset, List[Any]] = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -elif {{ source }} is None: - {{ destination }} = None -{% endif %} -else: + if {{ source }} is None: + {{ destination }} = None + else: + {{ _transform(property, source, destination) | indent(4)}} +{% else %} {{ _transform(property, source, destination) | indent(4)}} {% endif %} +{% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/ref_property.pyi b/openapi_python_client/templates/property_templates/ref_property.pyi index 90990a23e..c1fa0a3bb 100644 --- a/openapi_python_client/templates/property_templates/ref_property.pyi +++ b/openapi_python_client/templates/property_templates/ref_property.pyi @@ -16,13 +16,11 @@ if {{ source }} is not None: {{ destination }} = {{ source }}.to_dict() {% endif %} {% else %} -if {{ source }} is UNSET: - {{ destination }} = UNSET +{{ destination }}: {{ property.get_type_string() }} = UNSET +if not isinstance({{ source }}, Unset): {% if property.nullable %} -else: {{ destination }} = {{ source }}.to_dict() if {{ source }} else None {% else %} -else: {{ destination }} = {{ source }}.to_dict() {% endif %} {% endif %} diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index 710e2d1bb..036025079 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -23,8 +23,9 @@ def _parse_{{ property.python_name }}(data: Dict[str, Any]) -> {{ property.get_t {% macro transform(property, source, destination) %} {% if not property.required %} -if {{ source }} is UNSET: - {{ destination }}: {{ property.get_type_string() }} = UNSET +{{ destination }}: {{ property.get_type_string() }} +if isinstance({{ source }}, Unset): + {{ destination }} = UNSET {% endif %} {% if property.nullable %} {% if property.required %} diff --git a/openapi_python_client/templates/types.py b/openapi_python_client/templates/types.py index 99a344e09..7f0f544d8 100644 --- a/openapi_python_client/templates/types.py +++ b/openapi_python_client/templates/types.py @@ -1,10 +1,13 @@ """ Contains some shared types for properties """ -from typing import Any, BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union +from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union import attr + class Unset: pass + + UNSET: Unset = Unset() diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py index ff8332821..f666b4774 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -24,14 +24,18 @@ def test_get_type_string(self): p = Property(name="test", required=True, default=None, nullable=False) p._type_string = "TestType" - assert p.get_type_string() == "TestType" - p.required = False - assert p.get_type_string() == "TestType" - assert p.get_type_string(True) == "TestType" + base_type_string = f"TestType" + + assert p.get_type_string() == base_type_string - p.required = False p.nullable = True - assert p.get_type_string() == "Optional[TestType]" + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" def test_to_string(self, mocker): from openapi_python_client.parser.properties import Property @@ -43,7 +47,7 @@ def test_to_string(self, mocker): assert p.to_string() == f"{name}: {get_type_string()}" p.required = False - assert p.to_string() == f"{name}: {get_type_string()} = cast({get_type_string()}, UNSET)" + assert p.to_string() == f"{name}: {get_type_string()} = UNSET" p.required = True p.nullable = True @@ -59,7 +63,7 @@ def test_get_imports(self): assert p.get_imports(prefix="") == set() p.required = False - assert p.get_imports(prefix="") == {"from typing import cast, Optional", "from types import UNSET, Unset"} + assert p.get_imports(prefix="") == {"from typing import Union, Optional", "from types import UNSET, Unset"} def test__validate_default(self): from openapi_python_client.parser.properties import Property @@ -80,11 +84,18 @@ def test_get_type_string(self): p = StringProperty(name="test", required=True, default=None, nullable=False) - assert p.get_type_string() == "str" - p.required = False - assert p.get_type_string() == "str" + base_type_string = f"str" + + assert p.get_type_string() == base_type_string + p.nullable = True - assert p.get_type_string() == "Optional[str]" + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" def test__validate_default(self): from openapi_python_client.parser.properties import StringProperty @@ -109,7 +120,7 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -118,7 +129,7 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -148,7 +159,7 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -157,7 +168,7 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -184,14 +195,14 @@ def test_get_imports(self): p.required = False assert p.get_imports(prefix=prefix) == { "from ...types import File", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } p.nullable = True assert p.get_imports(prefix=prefix) == { "from ...types import File", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -253,13 +264,18 @@ def test_get_type_string(self, mocker): inner_property.get_type_string.return_value = inner_type_string p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=False) - assert p.get_type_string() == f"List[{inner_type_string}]" + base_type_string = f"List[{inner_type_string}]" - p.required = False - assert p.get_type_string() == f"List[{inner_type_string}]" + assert p.get_type_string() == base_type_string p.nullable = True - assert p.get_type_string() == f"Optional[List[{inner_type_string}]]" + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" def test_get_type_imports(self, mocker): from openapi_python_client.parser.properties import ListProperty @@ -279,7 +295,7 @@ def test_get_type_imports(self, mocker): assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -287,7 +303,7 @@ def test_get_type_imports(self, mocker): assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -316,13 +332,18 @@ def test_get_type_string(self, mocker): nullable=False, ) - assert p.get_type_string() == "Union[inner_type_string_1, inner_type_string_2]" + base_type_string = f"Union[inner_type_string_1, inner_type_string_2]" - p.required = False - assert p.get_type_string() == "Union[inner_type_string_1, inner_type_string_2]" + assert p.get_type_string() == base_type_string p.nullable = True - assert p.get_type_string() == "Optional[Union[inner_type_string_1, inner_type_string_2]]" + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" def test_get_type_imports(self, mocker): from openapi_python_client.parser.properties import UnionProperty @@ -353,7 +374,7 @@ def test_get_type_imports(self, mocker): inner_import_1, inner_import_2, "from typing import Union", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -362,7 +383,7 @@ def test_get_type_imports(self, mocker): inner_import_1, inner_import_2, "from typing import Union", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -452,17 +473,22 @@ def test_get_type_string(self, mocker): from openapi_python_client.parser import properties - enum_property = properties.EnumProperty( + p = properties.EnumProperty( name="test", required=True, default=None, values={}, title="a_title", nullable=False ) - assert enum_property.get_type_string() == "MyTestEnum" + base_type_string = f"MyTestEnum" - enum_property.required = False - assert enum_property.get_type_string() == "MyTestEnum" + assert p.get_type_string() == base_type_string - enum_property.nullable = True - assert enum_property.get_type_string() == "Optional[MyTestEnum]" + p.nullable = True + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" properties._existing_enums = {} @@ -484,14 +510,14 @@ def test_get_imports(self, mocker): enum_property.required = False assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } enum_property.nullable = True assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -576,7 +602,7 @@ def test_template(self, mocker): def test_get_type_string(self, mocker): from openapi_python_client.parser.properties import RefProperty - ref_property = RefProperty( + p = RefProperty( name="test", required=True, default=None, @@ -584,13 +610,18 @@ def test_get_type_string(self, mocker): nullable=False, ) - assert ref_property.get_type_string() == "MyRefClass" + base_type_string = f"MyRefClass" - ref_property.required = False - assert ref_property.get_type_string() == "MyRefClass" + assert p.get_type_string() == base_type_string - ref_property.nullable = True - assert ref_property.get_type_string() == "Optional[MyRefClass]" + p.nullable = True + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p.required = False + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p.nullable = False + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" def test_get_imports(self, mocker): fake_reference = mocker.MagicMock(class_name="MyRefClass", module_name="my_test_enum") @@ -611,7 +642,7 @@ def test_get_imports(self, mocker): f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", "from typing import Dict", "from typing import cast", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -620,7 +651,7 @@ def test_get_imports(self, mocker): f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", "from typing import Dict", "from typing import cast", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -650,14 +681,14 @@ def test_get_imports(self, mocker): p.required = False assert p.get_imports(prefix=prefix) == { "from typing import Dict", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } p.nullable = False assert p.get_imports(prefix=prefix) == { "from typing import Dict", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } @@ -666,7 +697,7 @@ def test_get_imports(self, mocker): "from typing import Dict", "from typing import cast", "from dataclasses import field", - "from typing import cast, Optional", + "from typing import Union, Optional", "from ...types import UNSET, Unset", } From 0ab3ddfc6109f7e65f65013b2780eed14d428a48 Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Fri, 6 Nov 2020 09:23:37 -0500 Subject: [PATCH 10/25] Apply suggestions from code review Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com> --- openapi_python_client/parser/properties.py | 38 +++++++++++-------- openapi_python_client/templates/model.pyi | 2 +- .../property_templates/union_property.pyi | 4 +- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 603337f92..7666827d4 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -44,17 +44,19 @@ def _validate_default(self, default: Any) -> Any: """ Check that the default value is valid for the property's type + perform any necessary sanitization """ raise ValidationError - def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: + def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property Args: - no_optional: Do not include Optional even if the value is optional (needed for isinstance checks) + no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) """ type_string = self._type_string - if not no_optional and self.nullable: + if no_optional: + return type_string + if self.nullable: type_string = f"Optional[{type_string}]" - if not no_unset and not self.required: + if not self.required: type_string = f"Union[Unset, {type_string}]" return type_string @@ -225,12 +227,14 @@ class ListProperty(Property, Generic[InnerProp]): inner_property: InnerProp template: ClassVar[str] = "list_property.pyi" - def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: + def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ type_string = f"List[{self.inner_property.get_type_string()}]" - if not no_optional and self.nullable: + if no_optional: + return type_string + if self.nullable: type_string = f"Optional[{type_string}]" - if not no_unset and not self.required: + if not self.required: type_string = f"Union[Unset, {type_string}]" return type_string @@ -258,9 +262,9 @@ class UnionProperty(Property): inner_properties: List[Property] template: ClassVar[str] = "union_property.pyi" - def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: + def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - inner_types = [p.get_type_string(no_unset=True) for p in self.inner_properties] + inner_types = [p.get_type_string(no_optional=True) for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) type_string = f"Union[{inner_prop_string}]" if not no_optional and self.nullable: @@ -337,12 +341,14 @@ def get_enum(name: str) -> Optional["EnumProperty"]: """ Get all the EnumProperties that have been registered keyed by class name """ return _existing_enums.get(name) - def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: + def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ type_string = self.reference.class_name - if not no_optional and self.nullable: + if no_optional: + return type_string + if self.nullable: type_string = f"Optional[{type_string}]" - if not no_unset and not self.required: + if not self.required: type_string = f"Union[Unset, {type_string}]" return type_string @@ -401,12 +407,14 @@ def template(self) -> str: # type: ignore return "enum_property.pyi" return "ref_property.pyi" - def get_type_string(self, no_optional: bool = False, no_unset: bool = False) -> str: + def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ type_string = self.reference.class_name - if not no_optional and self.nullable: + if no_optional: + return type_string + if self.nullable: type_string = f"Optional[{type_string}]" - if not no_unset and not self.required: + if not self.required: type_string = f"Union[Unset, {type_string}]" return type_string diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index 1bfcc81f0..df100e69d 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Set +from typing import Any, Dict import attr diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index 036025079..ba53528d6 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -37,9 +37,9 @@ elif {{ source }} is None: {% endif %} {% for inner_property in property.inner_properties %} {% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #} -if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True, no_unset=True) }}): +if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): {% elif not loop.last %} -elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True, no_unset=True) }}): +elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): {% else %} else: {% endif %} From 9e9ebbe6adb0d12be94cef532f6cd49cf8991bda Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Fri, 6 Nov 2020 09:51:24 -0500 Subject: [PATCH 11/25] Cleaned up union type strings when property is not required + required but nullable properties no longer have a default value --- .../api/tests/defaults_tests_defaults_post.py | 22 +++++++++---------- .../my_test_api_client/models/a_model.py | 4 ++-- openapi_python_client/parser/properties.py | 12 +++++----- openapi_python_client/templates/model.pyi | 7 ++++++ tests/test_openapi_parser/test_properties.py | 22 ++++++++++++++++--- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index a251e4a9e..0991b7f73 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -19,9 +19,9 @@ def _get_kwargs( date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), float_prop: Union[Unset, float] = 3.14, int_prop: Union[Unset, int] = 7, - boolean_prop: Union[Unset, bool] = UNSET, + boolean_prop: Union[Unset, bool] = False, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[float, str]] = "not a float", + union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) @@ -44,7 +44,7 @@ def _get_kwargs( json_list_prop.append(list_prop_item) - json_union_prop: Union[Unset, Union[float, str]] + json_union_prop: Union[Unset, float, str] if isinstance(union_prop, Unset): json_union_prop = UNSET elif isinstance(union_prop, float): @@ -114,9 +114,9 @@ def sync_detailed( date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), float_prop: Union[Unset, float] = 3.14, int_prop: Union[Unset, int] = 7, - boolean_prop: Union[Unset, bool] = UNSET, + boolean_prop: Union[Unset, bool] = False, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[float, str]] = "not a float", + union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( @@ -149,9 +149,9 @@ def sync( date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), float_prop: Union[Unset, float] = 3.14, int_prop: Union[Unset, int] = 7, - boolean_prop: Union[Unset, bool] = UNSET, + boolean_prop: Union[Unset, bool] = False, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[float, str]] = "not a float", + union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -180,9 +180,9 @@ async def asyncio_detailed( date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), float_prop: Union[Unset, float] = 3.14, int_prop: Union[Unset, int] = 7, - boolean_prop: Union[Unset, bool] = UNSET, + boolean_prop: Union[Unset, bool] = False, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[float, str]] = "not a float", + union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( @@ -214,9 +214,9 @@ async def asyncio( date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), float_prop: Union[Unset, float] = 3.14, int_prop: Union[Unset, int] = 7, - boolean_prop: Union[Unset, bool] = UNSET, + boolean_prop: Union[Unset, bool] = False, list_prop: Union[Unset, List[AnEnum]] = UNSET, - union_prop: Union[Unset, Union[float, str]] = "not a float", + union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, ) -> Optional[Union[None, HTTPValidationError]]: """ """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index e223bd393..a2fd94276 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -17,10 +17,10 @@ class AModel: a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date required_not_nullable: str + some_dict: Optional[Dict[Any, Any]] + required_nullable: Optional[str] nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET - some_dict: Optional[Dict[Any, Any]] = None attr_1_leading_digit: Union[Unset, str] = UNSET - required_nullable: Optional[str] = None not_required_nullable: Union[Unset, Optional[str]] = UNSET not_required_not_nullable: Union[Unset, str] = UNSET diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index 7666827d4..8fc4ac605 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -74,12 +74,10 @@ def get_imports(self, *, prefix: str) -> Set[str]: def to_string(self) -> str: """ How this should be declared in a dataclass """ - if self.default: + if self.default is not None: default = self.default elif not self.required: default = "UNSET" - elif self.nullable: - default = "None" else: default = None @@ -267,10 +265,12 @@ def get_type_string(self, no_optional: bool = False) -> str: inner_types = [p.get_type_string(no_optional=True) for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) type_string = f"Union[{inner_prop_string}]" - if not no_optional and self.nullable: + if no_optional: + return type_string + if not self.required: + type_string = f"Union[Unset, {inner_prop_string}]" + if self.nullable: type_string = f"Optional[{type_string}]" - if not no_unset and not self.required: - type_string = f"Union[Unset, {type_string}]" return type_string def get_imports(self, *, prefix: str) -> Set[str]: diff --git a/openapi_python_client/templates/model.pyi b/openapi_python_client/templates/model.pyi index df100e69d..cbed730f3 100644 --- a/openapi_python_client/templates/model.pyi +++ b/openapi_python_client/templates/model.pyi @@ -13,7 +13,14 @@ from ..types import UNSET, Unset class {{ model.reference.class_name }}: """ {{ model.description }} """ {% for property in model.required_properties + model.optional_properties %} + {% if property.default is none and property.required %} {{ property.to_string() }} + {% endif %} + {% endfor %} + {% for property in model.required_properties + model.optional_properties %} + {% if property.default is not none or not property.required %} + {{ property.to_string() }} + {% endif %} {% endfor %} def to_dict(self) -> Dict[str, Any]: diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py index f666b4774..892f4ec07 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -30,12 +30,15 @@ def test_get_type_string(self): p.nullable = True assert p.get_type_string() == f"Optional[{base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string p.required = False assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string(no_optional=True) == base_type_string p.nullable = False assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string def test_to_string(self, mocker): from openapi_python_client.parser.properties import Property @@ -51,7 +54,7 @@ def test_to_string(self, mocker): p.required = True p.nullable = True - assert p.to_string() == f"{name}: {get_type_string()} = None" + assert p.to_string() == f"{name}: {get_type_string()}" p.default = "TEST" assert p.to_string() == f"{name}: {get_type_string()} = TEST" @@ -270,12 +273,15 @@ def test_get_type_string(self, mocker): p.nullable = True assert p.get_type_string() == f"Optional[{base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string p.required = False assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string(no_optional=True) == base_type_string p.nullable = False assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string def test_get_type_imports(self, mocker): from openapi_python_client.parser.properties import ListProperty @@ -338,12 +344,16 @@ def test_get_type_string(self, mocker): p.nullable = True assert p.get_type_string() == f"Optional[{base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string + base_type_string_with_unset = f"Union[Unset, inner_type_string_1, inner_type_string_2]" p.required = False - assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string() == f"Optional[{base_type_string_with_unset}]" + assert p.get_type_string(no_optional=True) == base_type_string p.nullable = False - assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + assert p.get_type_string() == base_type_string_with_unset + assert p.get_type_string(no_optional=True) == base_type_string def test_get_type_imports(self, mocker): from openapi_python_client.parser.properties import UnionProperty @@ -483,12 +493,15 @@ def test_get_type_string(self, mocker): p.nullable = True assert p.get_type_string() == f"Optional[{base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string p.required = False assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string(no_optional=True) == base_type_string p.nullable = False assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string properties._existing_enums = {} @@ -616,12 +629,15 @@ def test_get_type_string(self, mocker): p.nullable = True assert p.get_type_string() == f"Optional[{base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string p.required = False assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + assert p.get_type_string(no_optional=True) == base_type_string p.nullable = False assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + assert p.get_type_string(no_optional=True) == base_type_string def test_get_imports(self, mocker): fake_reference = mocker.MagicMock(class_name="MyRefClass", module_name="my_test_enum") From a669ba633f2816a58519779b36cc779cb839b53b Mon Sep 17 00:00:00 2001 From: Ethan Mann Date: Fri, 6 Nov 2020 09:55:45 -0500 Subject: [PATCH 12/25] Regenerated the golden record --- .../api/tests/defaults_tests_defaults_post.py | 59 +++++++++-------- ...tional_value_tests_optional_query_param.py | 50 +++++++++++++++ .../tests/upload_file_tests_upload_post.py | 7 +- .../my_test_api_client/models/a_model.py | 64 ++++++++++++++----- .../body_upload_file_tests_upload_post.py | 4 +- .../models/http_validation_error.py | 20 +++--- .../models/validation_error.py | 4 +- .../my_test_api_client/types.py | 7 ++ 8 files changed, 158 insertions(+), 57 deletions(-) create mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 84347423e..ed054315d 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -11,6 +11,7 @@ from ...models.an_enum import AnEnum from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Unset def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: @@ -34,57 +35,63 @@ def httpx_request( *, client: Client, json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = False, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, float, str] = "not a float", + enum_prop: Union[Unset, AnEnum] = UNSET, ) -> httpx.Response[Union[None, HTTPValidationError]]: - json_datetime_prop = datetime_prop.isoformat() if datetime_prop else None + json_datetime_prop: Union[Unset, str] = UNSET + if not isinstance(datetime_prop, Unset): + json_datetime_prop = datetime_prop.isoformat() - json_date_prop = date_prop.isoformat() if date_prop else None + json_date_prop: Union[Unset, str] = UNSET + if not isinstance(date_prop, Unset): + json_date_prop = date_prop.isoformat() - if list_prop is None: - json_list_prop = None - else: + json_list_prop: Union[Unset, List[Any]] = UNSET + if not isinstance(list_prop, Unset): json_list_prop = [] for list_prop_item_data in list_prop: list_prop_item = list_prop_item_data.value json_list_prop.append(list_prop_item) - if union_prop is None: - json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None + json_union_prop: Union[Unset, float, str] + if isinstance(union_prop, Unset): + json_union_prop = UNSET elif isinstance(union_prop, float): json_union_prop = union_prop else: json_union_prop = union_prop - json_enum_prop = enum_prop.value if enum_prop else None + json_enum_prop: Union[Unset, AnEnum] = UNSET + if not isinstance(enum_prop, Unset): + json_enum_prop = enum_prop.value params: Dict[str, Any] = {} - if string_prop is not None: + if string_prop is not UNSET: params["string_prop"] = string_prop - if datetime_prop is not None: + if datetime_prop is not UNSET: params["datetime_prop"] = json_datetime_prop - if date_prop is not None: + if date_prop is not UNSET: params["date_prop"] = json_date_prop - if float_prop is not None: + if float_prop is not UNSET: params["float_prop"] = float_prop - if int_prop is not None: + if int_prop is not UNSET: params["int_prop"] = int_prop - if boolean_prop is not None: + if boolean_prop is not UNSET: params["boolean_prop"] = boolean_prop - if list_prop is not None: + if list_prop is not UNSET: params["list_prop"] = json_list_prop - if union_prop is not None: + if union_prop is not UNSET: params["union_prop"] = json_union_prop - if enum_prop is not None: + if enum_prop is not UNSET: params["enum_prop"] = json_enum_prop json_json_body = json_body diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py new file mode 100644 index 000000000..bce044ca0 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -0,0 +1,50 @@ +from typing import Optional + +import httpx + +Client = httpx.Client + +from typing import List, Optional, Union + +from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Unset + + +def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: + if response.status_code == 200: + return None + if response.status_code == 422: + return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + return None + + +def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, HTTPValidationError]]: + return httpx.Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=_parse_response(response=response), + ) + + +def httpx_request( + *, + client: Client, + query_param: Union[Unset, List[str]] = UNSET, +) -> httpx.Response[Union[None, HTTPValidationError]]: + + json_query_param: Union[Unset, List[Any]] = UNSET + if not isinstance(query_param, Unset): + json_query_param = query_param + + params: Dict[str, Any] = {} + if query_param is not UNSET: + params["query_param"] = json_query_param + + response = client.request( + "get", + "/tests/optional_query_param/", + params=params, + ) + + return _build_response(response=response) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 1ef04185b..e294e6fae 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -4,10 +4,11 @@ Client = httpx.Client -from typing import Optional +from typing import Optional, Union from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Unset def _parse_response(*, response: httpx.Response) -> Optional[Union[ @@ -37,12 +38,12 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[ def httpx_request(*, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: Union[Unset, bool] = UNSET, ) -> httpx.Response[Union[ None, HTTPValidationError ]]: - if keep_alive is not None: + if keep_alive is not UNSET: headers["keep-alive"] = keep_alive diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py index a1a0ace0c..a2fd94276 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py @@ -6,6 +6,7 @@ from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum +from ..types import UNSET, Unset @attr.s(auto_attribs=True) @@ -13,17 +14,19 @@ class AModel: """ A Model for testing all the ways custom objects can be used """ an_enum_value: AnEnum - some_dict: Optional[Dict[Any, Any]] a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date - nested_list_of_enums: Optional[List[List[DifferentEnum]]] = None - attr_1_leading_digit: Optional[str] = None + required_not_nullable: str + some_dict: Optional[Dict[Any, Any]] + required_nullable: Optional[str] + nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET + attr_1_leading_digit: Union[Unset, str] = UNSET + not_required_nullable: Union[Unset, Optional[str]] = UNSET + not_required_not_nullable: Union[Unset, str] = UNSET def to_dict(self) -> Dict[str, Any]: an_enum_value = self.an_enum_value.value - some_dict = self.some_dict - if isinstance(self.a_camel_date_time, datetime.datetime): a_camel_date_time = self.a_camel_date_time.isoformat() @@ -32,9 +35,9 @@ def to_dict(self) -> Dict[str, Any]: a_date = self.a_date.isoformat() - if self.nested_list_of_enums is None: - nested_list_of_enums = None - else: + required_not_nullable = self.required_not_nullable + nested_list_of_enums: Union[Unset, List[Any]] = UNSET + if not isinstance(self.nested_list_of_enums, Unset): nested_list_of_enums = [] for nested_list_of_enums_item_data in self.nested_list_of_enums: nested_list_of_enums_item = [] @@ -45,23 +48,36 @@ def to_dict(self) -> Dict[str, Any]: nested_list_of_enums.append(nested_list_of_enums_item) + some_dict = self.some_dict if self.some_dict else None + attr_1_leading_digit = self.attr_1_leading_digit + required_nullable = self.required_nullable + not_required_nullable = self.not_required_nullable + not_required_not_nullable = self.not_required_not_nullable - return { + field_dict = { "an_enum_value": an_enum_value, - "some_dict": some_dict, "aCamelDateTime": a_camel_date_time, "a_date": a_date, - "nested_list_of_enums": nested_list_of_enums, - "1_leading_digit": attr_1_leading_digit, + "required_not_nullable": required_not_nullable, + "some_dict": some_dict, + "required_nullable": required_nullable, } + if nested_list_of_enums is not UNSET: + field_dict["nested_list_of_enums"] = nested_list_of_enums + if attr_1_leading_digit is not UNSET: + field_dict["1_leading_digit"] = attr_1_leading_digit + if not_required_nullable is not UNSET: + field_dict["not_required_nullable"] = not_required_nullable + if not_required_not_nullable is not UNSET: + field_dict["not_required_not_nullable"] = not_required_not_nullable + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "AModel": an_enum_value = AnEnum(d["an_enum_value"]) - some_dict = d["some_dict"] - def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]: a_camel_date_time: Union[datetime.datetime, datetime.date] try: @@ -78,8 +94,10 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d a_date = isoparse(d["a_date"]).date() + required_not_nullable = d["required_not_nullable"] + nested_list_of_enums = [] - for nested_list_of_enums_item_data in d.get("nested_list_of_enums") or []: + for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) @@ -88,13 +106,25 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d nested_list_of_enums.append(nested_list_of_enums_item) - attr_1_leading_digit = d.get("1_leading_digit") + some_dict = d["some_dict"] + + attr_1_leading_digit = d.get("1_leading_digit", UNSET) + + required_nullable = d["required_nullable"] + + not_required_nullable = d.get("not_required_nullable", UNSET) + + not_required_not_nullable = d.get("not_required_not_nullable", UNSET) return AModel( an_enum_value=an_enum_value, - some_dict=some_dict, a_camel_date_time=a_camel_date_time, a_date=a_date, + required_not_nullable=required_not_nullable, nested_list_of_enums=nested_list_of_enums, + some_dict=some_dict, attr_1_leading_digit=attr_1_leading_digit, + required_nullable=required_nullable, + not_required_nullable=not_required_nullable, + not_required_not_nullable=not_required_not_nullable, ) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py index 4fe7f8476..3435bd290 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -14,10 +14,12 @@ class BodyUploadFileTestsUploadPost: def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() - return { + field_dict = { "some_file": some_file, } + return field_dict + @staticmethod def from_dict(d: Dict[str, Any]) -> "BodyUploadFileTestsUploadPost": some_file = d["some_file"] diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py index 90cd71e8c..9d29faa4d 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py @@ -1,34 +1,36 @@ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Union import attr from ..models.validation_error import ValidationError +from ..types import UNSET, Unset @attr.s(auto_attribs=True) class HTTPValidationError: """ """ - detail: Optional[List[ValidationError]] = None + detail: Union[Unset, List[ValidationError]] = UNSET def to_dict(self) -> Dict[str, Any]: - if self.detail is None: - detail = None - else: + detail: Union[Unset, List[Any]] = UNSET + if not isinstance(self.detail, Unset): detail = [] for detail_item_data in self.detail: detail_item = detail_item_data.to_dict() detail.append(detail_item) - return { - "detail": detail, - } + field_dict = {} + if detail is not UNSET: + field_dict["detail"] = detail + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": detail = [] - for detail_item_data in d.get("detail") or []: + for detail_item_data in d.get("detail", UNSET) or []: detail_item = ValidationError.from_dict(detail_item_data) detail.append(detail_item) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py index 1e415c476..77b9239ef 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py @@ -17,12 +17,14 @@ def to_dict(self) -> Dict[str, Any]: msg = self.msg type = self.type - return { + field_dict = { "loc": loc, "msg": msg, "type": type, } + return field_dict + @staticmethod def from_dict(d: Dict[str, Any]) -> "ValidationError": loc = d["loc"] diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/types.py b/end_to_end_tests/golden-record-custom/my_test_api_client/types.py index 951227435..7f0f544d8 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/types.py @@ -4,6 +4,13 @@ import attr +class Unset: + pass + + +UNSET: Unset = Unset() + + @attr.s(auto_attribs=True) class File: """ Contains information for file uploads """ From b6ea790dd297eb8e0c5e63f9fcf3f42fc291cd98 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Fri, 6 Nov 2020 08:26:26 -0700 Subject: [PATCH 13/25] Manually fix a bunch of tests after merging main and feature/unset into this branch. --- .../api/default/__init__.py | 0 .../api/tests/defaults_tests_defaults_post.py | 74 +++++----- .../api/tests/get_user_list.py | 8 +- .../api/tests/int_enum_tests_int_enum_post.py | 8 +- ...ional_value_tests_optional_query_param.py} | 20 ++- .../test_inline_objects.py} | 23 ++-- .../tests/upload_file_tests_upload_post.py | 7 +- .../my_test_api_client/models/__init__.py | 4 +- .../my_test_api_client/models/a_model.py | 130 ------------------ .../body_upload_file_tests_upload_post.py | 4 +- .../my_test_api_client/models/dict_prop.py | 18 +++ .../models/http_validation_error.py | 40 ------ .../my_test_api_client/models/json_body.py | 29 ++++ .../models/validation_error.py | 4 +- .../my_test_api_client/types.py | 7 + .../api/tests/defaults_tests_defaults_post.py | 38 +++-- .../api/tests/test_inline_objects.py | 6 +- .../my_test_api_client/models/__init__.py | 4 +- .../my_test_api_client/models/dict_prop.py | 4 +- .../my_test_api_client/models/json_body.py | 16 ++- .../parser/properties/property.py | 1 + .../property_templates/union_property.pyi | 4 +- pyproject.toml | 2 +- .../test_parser/test_properties/test_init.py | 61 ++++---- 24 files changed, 218 insertions(+), 294 deletions(-) delete mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/api/default/__init__.py rename end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/{json_body_tests_json_body_post.py => optional_value_tests_optional_query_param.py} (67%) rename end_to_end_tests/golden-record-custom/my_test_api_client/api/{default/ping_ping_get.py => tests/test_inline_objects.py} (51%) delete mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py create mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/models/dict_prop.py delete mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py create mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/models/json_body.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/default/__init__.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/default/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 84347423e..9704278b0 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -5,12 +5,14 @@ Client = httpx.Client import datetime -from typing import Dict, List, Optional, Union, cast +from typing import Dict, List, Union, cast from dateutil.parser import isoparse from ...models.an_enum import AnEnum +from ...models.dict_prop import DictProp from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Unset def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: @@ -33,61 +35,65 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, H def httpx_request( *, client: Client, - json_body: Dict[Any, Any], - string_prop: Optional[str] = "the default string", - datetime_prop: Optional[datetime.datetime] = isoparse("1010-10-10T00:00:00"), - date_prop: Optional[datetime.date] = isoparse("1010-10-10").date(), - float_prop: Optional[float] = 3.14, - int_prop: Optional[int] = 7, - boolean_prop: Optional[bool] = False, - list_prop: Optional[List[AnEnum]] = None, - union_prop: Optional[Union[Optional[float], Optional[str]]] = "not a float", - enum_prop: Optional[AnEnum] = None, + json_body: DictProp, + string_prop: Union[Unset, str] = "the default string", + datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), + date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), + float_prop: Union[Unset, float] = 3.14, + int_prop: Union[Unset, int] = 7, + boolean_prop: Union[Unset, bool] = UNSET, + list_prop: Union[Unset, List[AnEnum]] = UNSET, + union_prop: Union[Unset, Union[float, str]] = "not a float", + an_enum: AnEnum, ) -> httpx.Response[Union[None, HTTPValidationError]]: - json_datetime_prop = datetime_prop.isoformat() if datetime_prop else None + json_datetime_prop: Union[Unset, str] = UNSET + if not isinstance(datetime_prop, Unset): + json_datetime_prop = datetime_prop.isoformat() - json_date_prop = date_prop.isoformat() if date_prop else None + json_date_prop: Union[Unset, str] = UNSET + if not isinstance(date_prop, Unset): + json_date_prop = date_prop.isoformat() - if list_prop is None: - json_list_prop = None - else: + json_list_prop: Union[Unset, List[Any]] = UNSET + if not isinstance(list_prop, Unset): json_list_prop = [] - for list_prop_item_data in list_prop: - list_prop_item = list_prop_item_data.value + for an_enum_data in list_prop: + an_enum = an_enum_data.value - json_list_prop.append(list_prop_item) + json_list_prop.append(an_enum) - if union_prop is None: - json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None + json_union_prop: Union[Unset, Union[float, str]] + if isinstance(union_prop, Unset): + json_union_prop = UNSET elif isinstance(union_prop, float): json_union_prop = union_prop else: json_union_prop = union_prop - json_enum_prop = enum_prop.value if enum_prop else None + json_an_enum = an_enum.value - params: Dict[str, Any] = {} - if string_prop is not None: + params: Dict[str, Any] = { + "AnEnum": json_an_enum, + } + if string_prop is not UNSET: params["string_prop"] = string_prop - if datetime_prop is not None: + if datetime_prop is not UNSET: params["datetime_prop"] = json_datetime_prop - if date_prop is not None: + if date_prop is not UNSET: params["date_prop"] = json_date_prop - if float_prop is not None: + if float_prop is not UNSET: params["float_prop"] = float_prop - if int_prop is not None: + if int_prop is not UNSET: params["int_prop"] = int_prop - if boolean_prop is not None: + if boolean_prop is not UNSET: params["boolean_prop"] = boolean_prop - if list_prop is not None: + if list_prop is not UNSET: params["list_prop"] = json_list_prop - if union_prop is not None: + if union_prop is not UNSET: params["union_prop"] = json_union_prop - if enum_prop is not None: - params["enum_prop"] = json_enum_prop - json_json_body = json_body + json_json_body = json_body.to_dict() response = client.request( "post", diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py index d3a8591bc..5d2bb0292 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py @@ -5,7 +5,7 @@ Client = httpx.Client import datetime -from typing import Dict, List, Union, cast +from typing import List, Union, cast from ...models.a_model import AModel from ...models.an_enum import AnEnum @@ -37,10 +37,10 @@ def httpx_request( ) -> httpx.Response[Union[List[AModel], HTTPValidationError]]: json_an_enum_value = [] - for an_enum_value_item_data in an_enum_value: - an_enum_value_item = an_enum_value_item_data.value + for an_enum_data in an_enum_value: + an_enum = an_enum_data.value - json_an_enum_value.append(an_enum_value_item) + json_an_enum_value.append(an_enum) if isinstance(some_date, datetime.date): json_some_date = some_date.isoformat() diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py index 068e6e9c7..d60f28c75 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py @@ -4,8 +4,6 @@ Client = httpx.Client -from typing import Dict, cast - from ...models.an_int_enum import AnIntEnum from ...models.http_validation_error import HTTPValidationError @@ -30,13 +28,13 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, H def httpx_request( *, client: Client, - int_enum: AnIntEnum, + an_int_enum: AnIntEnum, ) -> httpx.Response[Union[None, HTTPValidationError]]: - json_int_enum = int_enum.value + json_an_int_enum = an_int_enum.value params: Dict[str, Any] = { - "int_enum": json_int_enum, + "AnIntEnum": json_an_int_enum, } response = client.request( diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py similarity index 67% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/json_body_tests_json_body_post.py rename to end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py index 76f4ffe84..274a28aff 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/json_body_tests_json_body_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -4,10 +4,10 @@ Client = httpx.Client -from typing import Dict, cast +from typing import List, Union -from ...models.a_model import AModel from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Unset def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: @@ -30,15 +30,21 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, H def httpx_request( *, client: Client, - json_body: AModel, + query_param: Union[Unset, List[str]] = UNSET, ) -> httpx.Response[Union[None, HTTPValidationError]]: - json_json_body = json_body.to_dict() + json_query_param: Union[Unset, List[Any]] = UNSET + if not isinstance(query_param, Unset): + json_query_param = query_param + + params: Dict[str, Any] = {} + if query_param is not UNSET: + params["query_param"] = json_query_param response = client.request( - "post", - "/tests/json_body", - json=json_json_body, + "get", + "/tests/optional_query_param/", + params=params, ) return _build_response(response=response) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/default/ping_ping_get.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py similarity index 51% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/default/ping_ping_get.py rename to end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py index 5e4cc9a2d..9d044a0fa 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/default/ping_ping_get.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py @@ -4,30 +4,33 @@ Client = httpx.Client +from ...models.json_body import JsonBody +from ...types import UNSET, Unset -def _parse_response(*, response: httpx.Response) -> Optional[bool]: - if response.status_code == 200: - return bool(response.text) - return None - -def _build_response(*, response: httpx.Response) -> httpx.Response[bool]: +def _build_response(*, response: httpx.Response) -> httpx.Response[None]: return httpx.Response( status_code=response.status_code, content=response.content, headers=response.headers, - parsed=_parse_response(response=response), + parsed=None, ) def httpx_request( *, client: Client, -) -> httpx.Response[bool]: + json_body: Optional[JsonBody], +) -> httpx.Response[None]: + + json_json_body: Optional[JsonBody] = UNSET + if not isinstance(json_body, Unset): + json_json_body = json_body.to_dict() response = client.request( - "get", - "/ping", + "post", + "/tests/inline_objects", + json=json_json_body, ) return _build_response(response=response) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py index 1ef04185b..e658e2f04 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -4,10 +4,11 @@ Client = httpx.Client -from typing import Optional +from typing import Union from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError +from ...types import UNSET, Unset def _parse_response(*, response: httpx.Response) -> Optional[Union[ @@ -37,12 +38,12 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[ def httpx_request(*, client: Client, multipart_data: BodyUploadFileTestsUploadPost, - keep_alive: Optional[bool] = None, + keep_alive: Union[Unset, bool] = UNSET, ) -> httpx.Response[Union[ None, HTTPValidationError ]]: - if keep_alive is not None: + if keep_alive is not UNSET: headers["keep-alive"] = keep_alive diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/__init__.py index 5cf14bb24..49e4332bf 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/__init__.py @@ -1,9 +1,9 @@ """ Contains all the data models used in inputs/outputs """ -from .a_model import AModel from .an_enum import AnEnum from .an_int_enum import AnIntEnum from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost +from .dict_prop import DictProp from .different_enum import DifferentEnum -from .http_validation_error import HTTPValidationError +from .json_body import JsonBody from .validation_error import ValidationError diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py deleted file mode 100644 index e223bd393..000000000 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py +++ /dev/null @@ -1,130 +0,0 @@ -import datetime -from typing import Any, Dict, List, Optional, Union - -import attr -from dateutil.parser import isoparse - -from ..models.an_enum import AnEnum -from ..models.different_enum import DifferentEnum -from ..types import UNSET, Unset - - -@attr.s(auto_attribs=True) -class AModel: - """ A Model for testing all the ways custom objects can be used """ - - an_enum_value: AnEnum - a_camel_date_time: Union[datetime.datetime, datetime.date] - a_date: datetime.date - required_not_nullable: str - nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET - some_dict: Optional[Dict[Any, Any]] = None - attr_1_leading_digit: Union[Unset, str] = UNSET - required_nullable: Optional[str] = None - not_required_nullable: Union[Unset, Optional[str]] = UNSET - not_required_not_nullable: Union[Unset, str] = UNSET - - def to_dict(self) -> Dict[str, Any]: - an_enum_value = self.an_enum_value.value - - if isinstance(self.a_camel_date_time, datetime.datetime): - a_camel_date_time = self.a_camel_date_time.isoformat() - - else: - a_camel_date_time = self.a_camel_date_time.isoformat() - - a_date = self.a_date.isoformat() - - required_not_nullable = self.required_not_nullable - nested_list_of_enums: Union[Unset, List[Any]] = UNSET - if not isinstance(self.nested_list_of_enums, Unset): - nested_list_of_enums = [] - for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value - - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - nested_list_of_enums.append(nested_list_of_enums_item) - - some_dict = self.some_dict if self.some_dict else None - - attr_1_leading_digit = self.attr_1_leading_digit - required_nullable = self.required_nullable - not_required_nullable = self.not_required_nullable - not_required_not_nullable = self.not_required_not_nullable - - field_dict = { - "an_enum_value": an_enum_value, - "aCamelDateTime": a_camel_date_time, - "a_date": a_date, - "required_not_nullable": required_not_nullable, - "some_dict": some_dict, - "required_nullable": required_nullable, - } - if nested_list_of_enums is not UNSET: - field_dict["nested_list_of_enums"] = nested_list_of_enums - if attr_1_leading_digit is not UNSET: - field_dict["1_leading_digit"] = attr_1_leading_digit - if not_required_nullable is not UNSET: - field_dict["not_required_nullable"] = not_required_nullable - if not_required_not_nullable is not UNSET: - field_dict["not_required_not_nullable"] = not_required_not_nullable - - return field_dict - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "AModel": - an_enum_value = AnEnum(d["an_enum_value"]) - - def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]: - a_camel_date_time: Union[datetime.datetime, datetime.date] - try: - a_camel_date_time = isoparse(d["aCamelDateTime"]) - - return a_camel_date_time - except: # noqa: E722 - pass - a_camel_date_time = isoparse(d["aCamelDateTime"]).date() - - return a_camel_date_time - - a_camel_date_time = _parse_a_camel_date_time(d["aCamelDateTime"]) - - a_date = isoparse(d["a_date"]).date() - - required_not_nullable = d["required_not_nullable"] - - nested_list_of_enums = [] - for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) - - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - nested_list_of_enums.append(nested_list_of_enums_item) - - some_dict = d["some_dict"] - - attr_1_leading_digit = d.get("1_leading_digit", UNSET) - - required_nullable = d["required_nullable"] - - not_required_nullable = d.get("not_required_nullable", UNSET) - - not_required_not_nullable = d.get("not_required_not_nullable", UNSET) - - return AModel( - an_enum_value=an_enum_value, - a_camel_date_time=a_camel_date_time, - a_date=a_date, - required_not_nullable=required_not_nullable, - nested_list_of_enums=nested_list_of_enums, - some_dict=some_dict, - attr_1_leading_digit=attr_1_leading_digit, - required_nullable=required_nullable, - not_required_nullable=not_required_nullable, - not_required_not_nullable=not_required_not_nullable, - ) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py index 4fe7f8476..3435bd290 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -14,10 +14,12 @@ class BodyUploadFileTestsUploadPost: def to_dict(self) -> Dict[str, Any]: some_file = self.some_file.to_tuple() - return { + field_dict = { "some_file": some_file, } + return field_dict + @staticmethod def from_dict(d: Dict[str, Any]) -> "BodyUploadFileTestsUploadPost": some_file = d["some_file"] diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/dict_prop.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/dict_prop.py new file mode 100644 index 000000000..39f4a0123 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/dict_prop.py @@ -0,0 +1,18 @@ +from typing import Any, Dict + +import attr + + +@attr.s(auto_attribs=True) +class DictProp: + """ """ + + def to_dict(self) -> Dict[str, Any]: + + field_dict = {} + + return field_dict + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "DictProp": + return DictProp() diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py deleted file mode 100644 index 9d29faa4d..000000000 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Any, Dict, List, Union - -import attr - -from ..models.validation_error import ValidationError -from ..types import UNSET, Unset - - -@attr.s(auto_attribs=True) -class HTTPValidationError: - """ """ - - detail: Union[Unset, List[ValidationError]] = UNSET - - def to_dict(self) -> Dict[str, Any]: - detail: Union[Unset, List[Any]] = UNSET - if not isinstance(self.detail, Unset): - detail = [] - for detail_item_data in self.detail: - detail_item = detail_item_data.to_dict() - - detail.append(detail_item) - - field_dict = {} - if detail is not UNSET: - field_dict["detail"] = detail - - return field_dict - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": - detail = [] - for detail_item_data in d.get("detail", UNSET) or []: - detail_item = ValidationError.from_dict(detail_item_data) - - detail.append(detail_item) - - return HTTPValidationError( - detail=detail, - ) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/json_body.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/json_body.py new file mode 100644 index 000000000..6d57d2d3f --- /dev/null +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/json_body.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, Union + +import attr + +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class JsonBody: + """ """ + + a_property: Union[Unset, str] = UNSET + + def to_dict(self) -> Dict[str, Any]: + a_property = self.a_property + + field_dict = {} + if a_property is not UNSET: + field_dict["a_property"] = a_property + + return field_dict + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "JsonBody": + a_property = d.get("a_property", UNSET) + + return JsonBody( + a_property=a_property, + ) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py index 1e415c476..77b9239ef 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py @@ -17,12 +17,14 @@ def to_dict(self) -> Dict[str, Any]: msg = self.msg type = self.type - return { + field_dict = { "loc": loc, "msg": msg, "type": type, } + return field_dict + @staticmethod def from_dict(d: Dict[str, Any]) -> "ValidationError": loc = d["loc"] diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/types.py b/end_to_end_tests/golden-record-custom/my_test_api_client/types.py index 951227435..7f0f544d8 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/types.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/types.py @@ -4,6 +4,13 @@ import attr +class Unset: + pass + + +UNSET: Unset = Unset() + + @attr.s(auto_attribs=True) class File: """ Contains information for file uploads """ diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 715ddb57b..964770ae2 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -14,7 +14,7 @@ def _get_kwargs( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -23,7 +23,7 @@ def _get_kwargs( boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, union_prop: Union[Unset, Union[float, str]] = "not a float", - enum_prop: Union[Unset, AnEnum] = UNSET, + an_enum: AnEnum, ) -> Dict[str, Any]: url = "{}/tests/defaults".format(client.base_url) @@ -53,11 +53,11 @@ def _get_kwargs( else: json_union_prop = union_prop - json_enum_prop: Union[Unset, AnEnum] = UNSET - if not isinstance(enum_prop, Unset): - json_enum_prop = enum_prop.value + json_an_enum = an_enum.value - params: Dict[str, Any] = {} + params: Dict[str, Any] = { + "AnEnum": json_an_enum, + } if string_prop is not UNSET: params["string_prop"] = string_prop if datetime_prop is not UNSET: @@ -74,8 +74,6 @@ def _get_kwargs( params["list_prop"] = json_list_prop if union_prop is not UNSET: params["union_prop"] = json_union_prop - if enum_prop is not UNSET: - params["enum_prop"] = json_enum_prop json_json_body = json_body.to_dict() @@ -109,7 +107,7 @@ def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPVal def sync_detailed( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -118,7 +116,7 @@ def sync_detailed( boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, union_prop: Union[Unset, Union[float, str]] = "not a float", - enum_prop: Union[Unset, AnEnum] = UNSET, + an_enum: AnEnum, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -131,7 +129,7 @@ def sync_detailed( boolean_prop=boolean_prop, list_prop=list_prop, union_prop=union_prop, - enum_prop=enum_prop, + an_enum=an_enum, ) response = httpx.post( @@ -144,7 +142,7 @@ def sync_detailed( def sync( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -153,7 +151,7 @@ def sync( boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, union_prop: Union[Unset, Union[float, str]] = "not a float", - enum_prop: Union[Unset, AnEnum] = UNSET, + an_enum: AnEnum, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -168,14 +166,14 @@ def sync( boolean_prop=boolean_prop, list_prop=list_prop, union_prop=union_prop, - enum_prop=enum_prop, + an_enum=an_enum, ).parsed async def asyncio_detailed( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -184,7 +182,7 @@ async def asyncio_detailed( boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, union_prop: Union[Unset, Union[float, str]] = "not a float", - enum_prop: Union[Unset, AnEnum] = UNSET, + an_enum: AnEnum, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, @@ -197,7 +195,7 @@ async def asyncio_detailed( boolean_prop=boolean_prop, list_prop=list_prop, union_prop=union_prop, - enum_prop=enum_prop, + an_enum=an_enum, ) async with httpx.AsyncClient() as _client: @@ -209,7 +207,7 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -218,7 +216,7 @@ async def asyncio( boolean_prop: Union[Unset, bool] = UNSET, list_prop: Union[Unset, List[AnEnum]] = UNSET, union_prop: Union[Unset, Union[float, str]] = "not a float", - enum_prop: Union[Unset, AnEnum] = UNSET, + an_enum: AnEnum, ) -> Optional[Union[None, HTTPValidationError]]: """ """ @@ -234,6 +232,6 @@ async def asyncio( boolean_prop=boolean_prop, list_prop=list_prop, union_prop=union_prop, - enum_prop=enum_prop, + an_enum=an_enum, ) ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py index 56bec3cfa..a1824178a 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py @@ -4,7 +4,7 @@ from ...client import Client from ...models.json_body import JsonBody -from ...types import Response +from ...types import UNSET, Response, Unset def _get_kwargs( @@ -16,7 +16,9 @@ def _get_kwargs( headers: Dict[str, Any] = client.get_headers() - json_json_body = json_body.to_dict() if json_body else None + json_json_body: Optional[JsonBody] = UNSET + if not isinstance(json_body, Unset): + json_json_body = json_body.to_dict() return { "url": url, 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 5cf14bb24..49e4332bf 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 @@ -1,9 +1,9 @@ """ Contains all the data models used in inputs/outputs """ -from .a_model import AModel from .an_enum import AnEnum from .an_int_enum import AnIntEnum from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost +from .dict_prop import DictProp from .different_enum import DifferentEnum -from .http_validation_error import HTTPValidationError +from .json_body import JsonBody from .validation_error import ValidationError diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py b/end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py index 37d2bfad9..39f4a0123 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py @@ -9,7 +9,9 @@ class DictProp: def to_dict(self) -> Dict[str, Any]: - return {} + field_dict = {} + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "DictProp": diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py b/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py index 4cbed755d..6d57d2d3f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py @@ -1,24 +1,28 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict, Union import attr +from ..types import UNSET, Unset + @attr.s(auto_attribs=True) class JsonBody: """ """ - a_property: Optional[str] = None + a_property: Union[Unset, str] = UNSET def to_dict(self) -> Dict[str, Any]: a_property = self.a_property - return { - "a_property": a_property, - } + field_dict = {} + if a_property is not UNSET: + field_dict["a_property"] = a_property + + return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "JsonBody": - a_property = d.get("a_property") + a_property = d.get("a_property", UNSET) return JsonBody( a_property=a_property, diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index 4eb914156..1d77c60d1 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -68,6 +68,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: if self.nullable: imports.add("from typing import Optional") if not self.required: + imports.add("from typing import Union") imports.add(f"from {prefix}types import UNSET, Unset") return imports diff --git a/openapi_python_client/templates/property_templates/union_property.pyi b/openapi_python_client/templates/property_templates/union_property.pyi index 036025079..ba53528d6 100644 --- a/openapi_python_client/templates/property_templates/union_property.pyi +++ b/openapi_python_client/templates/property_templates/union_property.pyi @@ -37,9 +37,9 @@ elif {{ source }} is None: {% endif %} {% for inner_property in property.inner_properties %} {% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #} -if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True, no_unset=True) }}): +if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): {% elif not loop.last %} -elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True, no_unset=True) }}): +elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): {% else %} else: {% endif %} diff --git a/pyproject.toml b/pyproject.toml index e699327a7..231f2158f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ isort .\ """ regen = "python -m end_to_end_tests.regen_golden_record" regen_custom = "python -m end_to_end_tests.regen_golden_record custom" -e2e = "pytest openapi_python_client end_to_end_tests" +e2e = "pytest openapi_python_client end_to_end_tests/test_end_to_end.py" re = """ task regen\ && task regen_custom\ diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 77938cbd1..ccbe3c45c 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -1,7 +1,7 @@ import pytest import openapi_python_client.schema as oai -from openapi_python_client.parser.errors import ParseError, PropertyError, ValidationError +from openapi_python_client.parser.errors import PropertyError, ValidationError MODULE_NAME = "openapi_python_client.parser.properties" @@ -51,7 +51,7 @@ def test_to_string(self, mocker): p.required = True p.nullable = True - assert p.to_string() == f"{name}: {get_type_string()} = None" + assert p.to_string() == f"{name}: {get_type_string()}" p.default = "TEST" assert p.to_string() == f"{name}: {get_type_string()} = TEST" @@ -63,7 +63,14 @@ def test_get_imports(self): assert p.get_imports(prefix="") == set() p.required = False - assert p.get_imports(prefix="") == {"from typing import Union, Optional", "from types import UNSET, Unset"} + assert p.get_imports(prefix="") == {"from types import UNSET, Unset", "from typing import Union"} + + p.nullable = True + assert p.get_imports(prefix="") == { + "from types import UNSET, Unset", + "from typing import Optional", + "from typing import Union", + } def test__validate_default(self): from openapi_python_client.parser.properties import Property @@ -120,7 +127,7 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import Union, Optional", + "from typing import Union", "from ...types import UNSET, Unset", } @@ -129,7 +136,8 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import Union, Optional", + "from typing import Union", + "from typing import Optional", "from ...types import UNSET, Unset", } @@ -159,7 +167,7 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import Union, Optional", + "from typing import Union", "from ...types import UNSET, Unset", } @@ -168,7 +176,8 @@ def test_get_imports(self): "import datetime", "from typing import cast", "from dateutil.parser import isoparse", - "from typing import Union, Optional", + "from typing import Union", + "from typing import Optional", "from ...types import UNSET, Unset", } @@ -195,14 +204,15 @@ def test_get_imports(self): p.required = False assert p.get_imports(prefix=prefix) == { "from ...types import File", - "from typing import Union, Optional", + "from typing import Union", "from ...types import UNSET, Unset", } p.nullable = True assert p.get_imports(prefix=prefix) == { "from ...types import File", - "from typing import Union, Optional", + "from typing import Union", + "from typing import Optional", "from ...types import UNSET, Unset", } @@ -295,7 +305,7 @@ def test_get_type_imports(self, mocker): assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", - "from typing import Union, Optional", + "from typing import Union", "from ...types import UNSET, Unset", } @@ -303,7 +313,8 @@ def test_get_type_imports(self, mocker): assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", - "from typing import Union, Optional", + "from typing import Union", + "from typing import Optional", "from ...types import UNSET, Unset", } @@ -374,7 +385,7 @@ def test_get_type_imports(self, mocker): inner_import_1, inner_import_2, "from typing import Union", - "from typing import Union, Optional", + "from typing import Union", "from ...types import UNSET, Unset", } @@ -383,7 +394,7 @@ def test_get_type_imports(self, mocker): inner_import_1, inner_import_2, "from typing import Union", - "from typing import Union, Optional", + "from typing import Optional", "from ...types import UNSET, Unset", } @@ -520,14 +531,15 @@ def test_get_imports(self, mocker): enum_property.required = False assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import Union, Optional", + "from typing import Union", "from ...types import UNSET, Unset", } enum_property.nullable = True assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", - "from typing import Union, Optional", + "from typing import Union", + "from typing import Optional", "from ...types import UNSET, Unset", } @@ -667,14 +679,15 @@ def test_get_imports(self, mocker): p.required = False assert p.get_imports(prefix=prefix) == { "from typing import Dict", - "from typing import Union, Optional", + "from typing import Union", "from ...types import UNSET, Unset", } - p.nullable = False + p.nullable = True assert p.get_imports(prefix=prefix) == { "from typing import Dict", - "from typing import Union, Optional", + "from typing import Union", + "from typing import Optional", "from ...types import UNSET, Unset", } @@ -683,7 +696,8 @@ def test_get_imports(self, mocker): "from typing import Dict", "from typing import cast", "from dataclasses import field", - "from typing import Union, Optional", + "from typing import Union", + "from typing import Optional", "from ...types import UNSET, Unset", } @@ -984,9 +998,9 @@ def test_property_from_data_union(self, mocker): IntProperty = mocker.patch(f"{MODULE_NAME}.IntProperty") mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) - from openapi_python_client.parser.properties import property_from_data + from openapi_python_client.parser.properties import Schemas, property_from_data - p = property_from_data(name=name, required=required, data=data) + p, s = property_from_data(name=name, required=required, data=data, schemas=Schemas()) FloatProperty.assert_called_once_with(name=name, required=required, default="0.0", nullable=False) IntProperty.assert_called_once_with(name=name, required=required, default="0", nullable=False) @@ -998,6 +1012,7 @@ def test_property_from_data_union(self, mocker): nullable=False, ) assert p == UnionProperty.return_value + assert s == Schemas() def test_property_from_data_union_bad_type(self, mocker): name = mocker.MagicMock() @@ -1005,9 +1020,9 @@ def test_property_from_data_union_bad_type(self, mocker): data = oai.Schema(anyOf=[{}]) mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) - from openapi_python_client.parser.properties import property_from_data + from openapi_python_client.parser.properties import Schemas, property_from_data - p = property_from_data(name=name, required=required, data=data) + p, s = property_from_data(name=name, required=required, data=data, schemas=Schemas()) assert p == PropertyError(detail=f"Invalid property in union {name}", data=oai.Schema()) From 5addb4ae10316311c27b6a1fdbf36152e0e3b5c5 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Fri, 6 Nov 2020 09:14:41 -0700 Subject: [PATCH 14/25] Fix naming and required/not required for shared enums/models --- .gitignore | 3 +- end_to_end_tests/custom_config.yml | 13 ++ .../api/tests/defaults_tests_defaults_post.py | 8 +- .../api/tests/get_user_list.py | 6 +- .../api/tests/int_enum_tests_int_enum_post.py | 6 +- ...tional_value_tests_optional_query_param.py | 2 +- .../tests/upload_file_tests_upload_post.py | 2 +- .../my_test_api_client/models/a_model.py | 130 ------------------ .../models/http_validation_error.py | 40 ------ .../api/tests/defaults_tests_defaults_post.py | 16 ++- .../api/tests/get_user_list.py | 6 +- .../api/tests/int_enum_tests_int_enum_post.py | 22 +-- .../my_test_api_client/models/a_model.py | 130 ------------------ end_to_end_tests/test_end_to_end.py | 4 +- .../parser/properties/__init__.py | 12 +- .../parser/properties/property.py | 2 +- .../test_parser/test_properties/test_init.py | 74 +++++++--- 17 files changed, 113 insertions(+), 363 deletions(-) create mode 100644 end_to_end_tests/custom_config.yml delete mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py delete mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py delete mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/a_model.py diff --git a/.gitignore b/.gitignore index bba4f232a..5097b9891 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,5 @@ test-reports/ htmlcov/ # Generated end to end test data -my-test-api-client \ No newline at end of file +my-test-api-client/ +custom-e2e/ \ No newline at end of file diff --git a/end_to_end_tests/custom_config.yml b/end_to_end_tests/custom_config.yml new file mode 100644 index 000000000..4395fbb6c --- /dev/null +++ b/end_to_end_tests/custom_config.yml @@ -0,0 +1,13 @@ +project_name_override: "custom-e2e" +package_name_override: "custom_e2e" +class_overrides: + _ABCResponse: + class_name: ABCResponse + module_name: abc_response + AnEnumValueItem: + class_name: AnEnumValue + module_name: an_enum_value + NestedListOfEnumsItemItem: + class_name: AnEnumValue + module_name: an_enum_value +field_prefix: attr_ diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py index f511dd3ad..e262edbd7 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -35,7 +35,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, H def httpx_request( *, client: Client, - json_body: Dict[Any, Any], + json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -58,10 +58,10 @@ def httpx_request( json_list_prop: Union[Unset, List[Any]] = UNSET if not isinstance(list_prop, Unset): json_list_prop = [] - for an_enum_data in list_prop: - an_enum = an_enum_data.value + for list_prop_item_data in list_prop: + list_prop_item = list_prop_item_data.value - json_list_prop.append(an_enum) + json_list_prop.append(list_prop_item) json_union_prop: Union[Unset, float, str] if isinstance(union_prop, Unset): diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py index 5d2bb0292..951ea518a 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py @@ -37,10 +37,10 @@ def httpx_request( ) -> httpx.Response[Union[List[AModel], HTTPValidationError]]: json_an_enum_value = [] - for an_enum_data in an_enum_value: - an_enum = an_enum_data.value + for an_enum_value_item_data in an_enum_value: + an_enum_value_item = an_enum_value_item_data.value - json_an_enum_value.append(an_enum) + json_an_enum_value.append(an_enum_value_item) if isinstance(some_date, datetime.date): json_some_date = some_date.isoformat() diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py index d60f28c75..73f4c7605 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py @@ -28,13 +28,13 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, H def httpx_request( *, client: Client, - an_int_enum: AnIntEnum, + int_enum: AnIntEnum, ) -> httpx.Response[Union[None, HTTPValidationError]]: - json_an_int_enum = an_int_enum.value + json_int_enum = int_enum.value params: Dict[str, Any] = { - "AnIntEnum": json_an_int_enum, + "int_enum": json_int_enum, } response = client.request( diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py index bce044ca0..274a28aff 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -4,7 +4,7 @@ Client = httpx.Client -from typing import List, Optional, Union +from typing import List, Union from ...models.http_validation_error import HTTPValidationError from ...types import UNSET, Unset diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py index e294e6fae..e658e2f04 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -4,7 +4,7 @@ Client = httpx.Client -from typing import Optional, Union +from typing import Union from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py deleted file mode 100644 index a2fd94276..000000000 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py +++ /dev/null @@ -1,130 +0,0 @@ -import datetime -from typing import Any, Dict, List, Optional, Union - -import attr -from dateutil.parser import isoparse - -from ..models.an_enum import AnEnum -from ..models.different_enum import DifferentEnum -from ..types import UNSET, Unset - - -@attr.s(auto_attribs=True) -class AModel: - """ A Model for testing all the ways custom objects can be used """ - - an_enum_value: AnEnum - a_camel_date_time: Union[datetime.datetime, datetime.date] - a_date: datetime.date - required_not_nullable: str - some_dict: Optional[Dict[Any, Any]] - required_nullable: Optional[str] - nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET - attr_1_leading_digit: Union[Unset, str] = UNSET - not_required_nullable: Union[Unset, Optional[str]] = UNSET - not_required_not_nullable: Union[Unset, str] = UNSET - - def to_dict(self) -> Dict[str, Any]: - an_enum_value = self.an_enum_value.value - - if isinstance(self.a_camel_date_time, datetime.datetime): - a_camel_date_time = self.a_camel_date_time.isoformat() - - else: - a_camel_date_time = self.a_camel_date_time.isoformat() - - a_date = self.a_date.isoformat() - - required_not_nullable = self.required_not_nullable - nested_list_of_enums: Union[Unset, List[Any]] = UNSET - if not isinstance(self.nested_list_of_enums, Unset): - nested_list_of_enums = [] - for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value - - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - nested_list_of_enums.append(nested_list_of_enums_item) - - some_dict = self.some_dict if self.some_dict else None - - attr_1_leading_digit = self.attr_1_leading_digit - required_nullable = self.required_nullable - not_required_nullable = self.not_required_nullable - not_required_not_nullable = self.not_required_not_nullable - - field_dict = { - "an_enum_value": an_enum_value, - "aCamelDateTime": a_camel_date_time, - "a_date": a_date, - "required_not_nullable": required_not_nullable, - "some_dict": some_dict, - "required_nullable": required_nullable, - } - if nested_list_of_enums is not UNSET: - field_dict["nested_list_of_enums"] = nested_list_of_enums - if attr_1_leading_digit is not UNSET: - field_dict["1_leading_digit"] = attr_1_leading_digit - if not_required_nullable is not UNSET: - field_dict["not_required_nullable"] = not_required_nullable - if not_required_not_nullable is not UNSET: - field_dict["not_required_not_nullable"] = not_required_not_nullable - - return field_dict - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "AModel": - an_enum_value = AnEnum(d["an_enum_value"]) - - def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]: - a_camel_date_time: Union[datetime.datetime, datetime.date] - try: - a_camel_date_time = isoparse(d["aCamelDateTime"]) - - return a_camel_date_time - except: # noqa: E722 - pass - a_camel_date_time = isoparse(d["aCamelDateTime"]).date() - - return a_camel_date_time - - a_camel_date_time = _parse_a_camel_date_time(d["aCamelDateTime"]) - - a_date = isoparse(d["a_date"]).date() - - required_not_nullable = d["required_not_nullable"] - - nested_list_of_enums = [] - for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) - - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - nested_list_of_enums.append(nested_list_of_enums_item) - - some_dict = d["some_dict"] - - attr_1_leading_digit = d.get("1_leading_digit", UNSET) - - required_nullable = d["required_nullable"] - - not_required_nullable = d.get("not_required_nullable", UNSET) - - not_required_not_nullable = d.get("not_required_not_nullable", UNSET) - - return AModel( - an_enum_value=an_enum_value, - a_camel_date_time=a_camel_date_time, - a_date=a_date, - required_not_nullable=required_not_nullable, - nested_list_of_enums=nested_list_of_enums, - some_dict=some_dict, - attr_1_leading_digit=attr_1_leading_digit, - required_nullable=required_nullable, - not_required_nullable=not_required_nullable, - not_required_not_nullable=not_required_not_nullable, - ) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py deleted file mode 100644 index 9d29faa4d..000000000 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py +++ /dev/null @@ -1,40 +0,0 @@ -from typing import Any, Dict, List, Union - -import attr - -from ..models.validation_error import ValidationError -from ..types import UNSET, Unset - - -@attr.s(auto_attribs=True) -class HTTPValidationError: - """ """ - - detail: Union[Unset, List[ValidationError]] = UNSET - - def to_dict(self) -> Dict[str, Any]: - detail: Union[Unset, List[Any]] = UNSET - if not isinstance(self.detail, Unset): - detail = [] - for detail_item_data in self.detail: - detail_item = detail_item_data.to_dict() - - detail.append(detail_item) - - field_dict = {} - if detail is not UNSET: - field_dict["detail"] = detail - - return field_dict - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": - detail = [] - for detail_item_data in d.get("detail", UNSET) or []: - detail_item = ValidationError.from_dict(detail_item_data) - - detail.append(detail_item) - - return HTTPValidationError( - detail=detail, - ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index a832406e9..4d87ca628 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -40,10 +40,10 @@ def _get_kwargs( json_list_prop: Union[Unset, List[Any]] = UNSET if not isinstance(list_prop, Unset): json_list_prop = [] - for an_enum_data in list_prop: - an_enum = an_enum_data.value + for list_prop_item_data in list_prop: + list_prop_item = list_prop_item_data.value - json_list_prop.append(an_enum) + json_list_prop.append(list_prop_item) json_union_prop: Union[Unset, float, str] if isinstance(union_prop, Unset): @@ -53,11 +53,11 @@ def _get_kwargs( else: json_union_prop = union_prop - json_an_enum = enum_prop.value + json_enum_prop: Union[Unset, AnEnum] = UNSET + if not isinstance(enum_prop, Unset): + json_enum_prop = enum_prop.value - params: Dict[str, Any] = { - "AnEnum": json_an_enum, - } + params: Dict[str, Any] = {} if string_prop is not UNSET: params["string_prop"] = string_prop if datetime_prop is not UNSET: @@ -74,6 +74,8 @@ def _get_kwargs( params["list_prop"] = json_list_prop if union_prop is not UNSET: params["union_prop"] = json_union_prop + if enum_prop is not UNSET: + params["enum_prop"] = json_enum_prop json_json_body = json_body.to_dict() diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py index 14b0327fb..815e0b02e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py @@ -21,10 +21,10 @@ def _get_kwargs( headers: Dict[str, Any] = client.get_headers() json_an_enum_value = [] - for an_enum_data in an_enum_value: - an_enum = an_enum_data.value + for an_enum_value_item_data in an_enum_value: + an_enum_value_item = an_enum_value_item_data.value - json_an_enum_value.append(an_enum) + json_an_enum_value.append(an_enum_value_item) if isinstance(some_date, datetime.date): json_some_date = some_date.isoformat() diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py index 1ea8a7453..e15ce2e2c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py @@ -11,16 +11,16 @@ def _get_kwargs( *, client: Client, - an_int_enum: AnIntEnum, + int_enum: AnIntEnum, ) -> Dict[str, Any]: url = "{}/tests/int_enum".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - json_an_int_enum = an_int_enum.value + json_int_enum = int_enum.value params: Dict[str, Any] = { - "AnIntEnum": json_an_int_enum, + "int_enum": json_int_enum, } return { @@ -52,11 +52,11 @@ def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPVal def sync_detailed( *, client: Client, - an_int_enum: AnIntEnum, + int_enum: AnIntEnum, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, - an_int_enum=an_int_enum, + int_enum=int_enum, ) response = httpx.post( @@ -69,24 +69,24 @@ def sync_detailed( def sync( *, client: Client, - an_int_enum: AnIntEnum, + int_enum: AnIntEnum, ) -> Optional[Union[None, HTTPValidationError]]: """ """ return sync_detailed( client=client, - an_int_enum=an_int_enum, + int_enum=int_enum, ).parsed async def asyncio_detailed( *, client: Client, - an_int_enum: AnIntEnum, + int_enum: AnIntEnum, ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, - an_int_enum=an_int_enum, + int_enum=int_enum, ) async with httpx.AsyncClient() as _client: @@ -98,13 +98,13 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - an_int_enum: AnIntEnum, + int_enum: AnIntEnum, ) -> Optional[Union[None, HTTPValidationError]]: """ """ return ( await asyncio_detailed( client=client, - an_int_enum=an_int_enum, + int_enum=int_enum, ) ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py deleted file mode 100644 index a2fd94276..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ /dev/null @@ -1,130 +0,0 @@ -import datetime -from typing import Any, Dict, List, Optional, Union - -import attr -from dateutil.parser import isoparse - -from ..models.an_enum import AnEnum -from ..models.different_enum import DifferentEnum -from ..types import UNSET, Unset - - -@attr.s(auto_attribs=True) -class AModel: - """ A Model for testing all the ways custom objects can be used """ - - an_enum_value: AnEnum - a_camel_date_time: Union[datetime.datetime, datetime.date] - a_date: datetime.date - required_not_nullable: str - some_dict: Optional[Dict[Any, Any]] - required_nullable: Optional[str] - nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET - attr_1_leading_digit: Union[Unset, str] = UNSET - not_required_nullable: Union[Unset, Optional[str]] = UNSET - not_required_not_nullable: Union[Unset, str] = UNSET - - def to_dict(self) -> Dict[str, Any]: - an_enum_value = self.an_enum_value.value - - if isinstance(self.a_camel_date_time, datetime.datetime): - a_camel_date_time = self.a_camel_date_time.isoformat() - - else: - a_camel_date_time = self.a_camel_date_time.isoformat() - - a_date = self.a_date.isoformat() - - required_not_nullable = self.required_not_nullable - nested_list_of_enums: Union[Unset, List[Any]] = UNSET - if not isinstance(self.nested_list_of_enums, Unset): - nested_list_of_enums = [] - for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value - - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - nested_list_of_enums.append(nested_list_of_enums_item) - - some_dict = self.some_dict if self.some_dict else None - - attr_1_leading_digit = self.attr_1_leading_digit - required_nullable = self.required_nullable - not_required_nullable = self.not_required_nullable - not_required_not_nullable = self.not_required_not_nullable - - field_dict = { - "an_enum_value": an_enum_value, - "aCamelDateTime": a_camel_date_time, - "a_date": a_date, - "required_not_nullable": required_not_nullable, - "some_dict": some_dict, - "required_nullable": required_nullable, - } - if nested_list_of_enums is not UNSET: - field_dict["nested_list_of_enums"] = nested_list_of_enums - if attr_1_leading_digit is not UNSET: - field_dict["1_leading_digit"] = attr_1_leading_digit - if not_required_nullable is not UNSET: - field_dict["not_required_nullable"] = not_required_nullable - if not_required_not_nullable is not UNSET: - field_dict["not_required_not_nullable"] = not_required_not_nullable - - return field_dict - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "AModel": - an_enum_value = AnEnum(d["an_enum_value"]) - - def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]: - a_camel_date_time: Union[datetime.datetime, datetime.date] - try: - a_camel_date_time = isoparse(d["aCamelDateTime"]) - - return a_camel_date_time - except: # noqa: E722 - pass - a_camel_date_time = isoparse(d["aCamelDateTime"]).date() - - return a_camel_date_time - - a_camel_date_time = _parse_a_camel_date_time(d["aCamelDateTime"]) - - a_date = isoparse(d["a_date"]).date() - - required_not_nullable = d["required_not_nullable"] - - nested_list_of_enums = [] - for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) - - nested_list_of_enums_item.append(nested_list_of_enums_item_item) - - nested_list_of_enums.append(nested_list_of_enums_item) - - some_dict = d["some_dict"] - - attr_1_leading_digit = d.get("1_leading_digit", UNSET) - - required_nullable = d["required_nullable"] - - not_required_nullable = d.get("not_required_nullable", UNSET) - - not_required_not_nullable = d.get("not_required_not_nullable", UNSET) - - return AModel( - an_enum_value=an_enum_value, - a_camel_date_time=a_camel_date_time, - a_date=a_date, - required_not_nullable=required_not_nullable, - nested_list_of_enums=nested_list_of_enums, - some_dict=some_dict, - attr_1_leading_digit=attr_1_leading_digit, - required_nullable=required_nullable, - not_required_nullable=not_required_nullable, - not_required_not_nullable=not_required_not_nullable, - ) diff --git a/end_to_end_tests/test_end_to_end.py b/end_to_end_tests/test_end_to_end.py index ce4942f7f..d30e0a687 100644 --- a/end_to_end_tests/test_end_to_end.py +++ b/end_to_end_tests/test_end_to_end.py @@ -52,9 +52,9 @@ def test_end_to_end(): def test_end_to_end_w_custom_templates(): runner = CliRunner() openapi_path = Path(__file__).parent / "openapi.json" - config_path = Path(__file__).parent / "config.yml" + config_path = Path(__file__).parent / "custom_config.yml" gr_path = Path(__file__).parent / "golden-record-custom" - output_path = Path.cwd() / "my-test-api-client" + output_path = Path.cwd() / "custom-e2e" shutil.rmtree(output_path, ignore_errors=True) result = runner.invoke( diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 6dd15c616..0644938bb 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -194,10 +194,10 @@ def get_type_string(self, no_optional: bool = False) -> str: type_string = f"Union[{inner_prop_string}]" if no_optional: return type_string + if not self.required: + type_string = f"Union[Unset, {inner_prop_string}]" if self.nullable: type_string = f"Optional[{type_string}]" - if not self.required: - type_string = f"Union[Unset, {type_string}]" return type_string def get_imports(self, *, prefix: str) -> Set[str]: @@ -399,9 +399,13 @@ def _property_from_data( if isinstance(data, oai.Reference): reference = Reference.from_ref(data.ref) if reference.class_name in schemas.enums: - return schemas.enums[reference.class_name], schemas + existing = schemas.enums[reference.class_name] + return ( + replace(existing, required=required, name=name, title=reference.class_name, existing_enums={}), + schemas, + ) elif reference.class_name in schemas.models: - return schemas.models[reference.class_name], schemas + return replace(schemas.models[reference.class_name], required=required, name=name), schemas else: return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas if data.enum: diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index 1d77c60d1..f282c22ef 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -74,7 +74,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: def to_string(self) -> str: """ How this should be declared in a dataclass """ - if self.default: + if self.default is not None: default = self.default elif not self.required: default = "UNSET" diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 2e3045e7e..5fe396fd8 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -754,37 +754,67 @@ def test_property_from_data_enum(self, mocker): } def test_property_from_data_ref_enum(self, mocker): - from openapi_python_client.parser.properties import Schemas, property_from_data + from openapi_python_client.parser.properties import EnumProperty, Schemas, property_from_data - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Reference.construct(ref=mocker.MagicMock()) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) - enum = mocker.MagicMock() - schemas = Schemas(enums={from_ref.return_value.class_name: enum}) + name = "some_enum" + data = oai.Reference.construct(ref="MyEnum") + existing_enum = EnumProperty( + name="an_enum", + required=True, + nullable=False, + default=None, + values={"A": "a"}, + title="MyEnum", + existing_enums={}, + ) + schemas = Schemas(enums={"MyEnum": existing_enum}) - prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) + prop, new_schemas = property_from_data(name=name, required=False, data=data, schemas=schemas) - from_ref.assert_called_once_with(data.ref) - assert prop == enum + assert prop == EnumProperty( + name="some_enum", + required=False, + nullable=False, + default=None, + values={"A": "a"}, + title="MyEnum", + existing_enums={}, + ) assert schemas == new_schemas def test_property_from_data_ref_model(self, mocker): - from openapi_python_client.parser.properties import Schemas, property_from_data - - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Reference.construct(ref=mocker.MagicMock()) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) - model = mocker.MagicMock() - schemas = Schemas(models={from_ref.return_value.class_name: model}) + from openapi_python_client.parser.properties import ModelProperty, Reference, Schemas, property_from_data + + name = "new_name" + required = False + class_name = "MyModel" + data = oai.Reference.construct(ref=class_name) + existing_model = ModelProperty( + name="old_name", + required=True, + nullable=False, + default=None, + reference=Reference(class_name=class_name, module_name="my_model"), + required_properties=[], + optional_properties=[], + description="", + relative_imports=set(), + ) + schemas = Schemas(models={class_name: existing_model}) prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) - from_ref.assert_called_once_with(data.ref) - assert prop == model + assert prop == ModelProperty( + name=name, + required=required, + nullable=False, + default=None, + reference=Reference(class_name=class_name, module_name="my_model"), + required_properties=[], + optional_properties=[], + description="", + relative_imports=set(), + ) assert schemas == new_schemas def test_property_from_data_ref_not_found(self, mocker): From dc6971d116a3ed99a9430c26d8ed660abe86a984 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Fri, 6 Nov 2020 16:14:19 -0700 Subject: [PATCH 15/25] Switch properties to use attr.s instead of dataclass --- .../parser/properties/__init__.py | 173 ++--- .../parser/properties/converter.py | 82 +++ .../parser/properties/enum_property.py | 41 +- .../parser/properties/model_property.py | 5 +- .../parser/properties/property.py | 26 +- .../parser/properties/schemas.py | 11 +- .../test_parser/test_properties/test_init.py | 640 +++++++----------- 7 files changed, 415 insertions(+), 563 deletions(-) create mode 100644 openapi_python_client/parser/properties/converter.py diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 0644938bb..5c5717864 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -1,33 +1,29 @@ -from dataclasses import dataclass, replace from itertools import chain from typing import Any, ClassVar, Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union -from dateutil.parser import isoparse +import attr from ... import schema as oai from ... import utils from ..errors import PropertyError, ValidationError from ..reference import Reference +from .converter import convert, convert_chain from .enum_property import EnumProperty from .model_property import ModelProperty from .property import Property from .schemas import Schemas -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class StringProperty(Property): """ A property of type str """ max_length: Optional[int] = None pattern: Optional[str] = None - _type_string: ClassVar[str] = "str" - def _validate_default(self, default: Any) -> str: - return f"{utils.remove_string_escapes(default)!r}" - -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class DateTimeProperty(Property): """ A property of type datetime.datetime @@ -48,15 +44,8 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports.update({"import datetime", "from typing import cast", "from dateutil.parser import isoparse"}) return imports - def _validate_default(self, default: Any) -> str: - try: - isoparse(default) - except (TypeError, ValueError) as e: - raise ValidationError from e - return f"isoparse({default!r})" - -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class DateProperty(Property): """ A property of type datetime.date """ @@ -75,15 +64,8 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports.update({"import datetime", "from typing import cast", "from dateutil.parser import isoparse"}) return imports - def _validate_default(self, default: Any) -> str: - try: - isoparse(default).date() - except (TypeError, ValueError) as e: - raise ValidationError() from e - return f"isoparse({default!r}).date()" - -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class FileProperty(Property): """ A property used for uploading files """ @@ -103,49 +85,31 @@ def get_imports(self, *, prefix: str) -> Set[str]: return imports -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class FloatProperty(Property): """ A property of type float """ - default: Optional[float] = None _type_string: ClassVar[str] = "float" - def _validate_default(self, default: Any) -> float: - try: - return float(default) - except (TypeError, ValueError) as e: - raise ValidationError() from e - -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class IntProperty(Property): """ A property of type int """ - default: Optional[int] = None _type_string: ClassVar[str] = "int" - def _validate_default(self, default: Any) -> int: - try: - return int(default) - except (TypeError, ValueError) as e: - raise ValidationError() from e - -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class BooleanProperty(Property): """ Property for bool """ _type_string: ClassVar[str] = "bool" - def _validate_default(self, default: Any) -> bool: - # no try/except needed as anything that comes from the initial load from json/yaml will be boolable - return bool(default) - InnerProp = TypeVar("InnerProp", bound=Property) -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class ListProperty(Property, Generic[InnerProp]): """ A property representing a list (array) of other properties """ @@ -176,11 +140,8 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports.add("from typing import List") return imports - def _validate_default(self, default: Any) -> None: - return None - -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class UnionProperty(Property): """ A property representing a Union (anyOf) of other properties """ @@ -214,41 +175,6 @@ def get_imports(self, *, prefix: str) -> Set[str]: imports.add("from typing import Union") return imports - def _validate_default(self, default: Any) -> Any: - for property in self.inner_properties: - try: - val = property._validate_default(default) - return val - except ValidationError: - continue - raise ValidationError() - - -@dataclass -class DictProperty(Property): - """ Property that is a general Dict """ - - _type_string: ClassVar[str] = "Dict[Any, Any]" - template: ClassVar[str] = "dict_property.pyi" - - def get_imports(self, *, prefix: str) -> Set[str]: - """ - Get a set of import strings that should be included when this property is used somewhere - - Args: - prefix: A prefix to put before any relative (local) module names. This should be the number of . to get - back to the root of the generated client. - """ - imports = super().get_imports(prefix=prefix) - imports.add("from typing import Dict") - if self.default is not None: - imports.add("from dataclasses import field") - imports.add("from typing import cast") - return imports - - def _validate_default(self, default: Any) -> None: - return None - def _string_based_property( name: str, required: bool, data: oai.Schema @@ -259,27 +185,27 @@ def _string_based_property( return DateTimeProperty( name=name, required=required, - default=data.default, + default=convert("datetime.datetime", data.default), nullable=data.nullable, ) elif string_format == "date": return DateProperty( name=name, required=required, - default=data.default, + default=convert("datetime.date", data.default), nullable=data.nullable, ) elif string_format == "binary": return FileProperty( name=name, required=required, - default=data.default, + default=None, nullable=data.nullable, ) else: return StringProperty( name=name, - default=data.default, + default=convert("str", data.default), required=required, pattern=data.pattern, nullable=data.nullable, @@ -327,23 +253,54 @@ def build_model_property( required=required, name=name, ) - schemas = replace(schemas, models={**schemas.models, prop.reference.class_name: prop}) + schemas = attr.evolve(schemas, models={**schemas.models, prop.reference.class_name: prop}) return prop, schemas def build_enum_property( *, data: oai.Schema, name: str, required: bool, schemas: Schemas, enum: List[Union[str, int]] -) -> Tuple[EnumProperty, Schemas]: +) -> Tuple[Union[EnumProperty, PropertyError], Schemas]: + + reference = Reference.from_ref(data.title or name) + values = EnumProperty.values_from_list(enum) + + dedup_counter = 0 # TODO: use the parent names instead of a counter for deduping + while reference.class_name in schemas.enums: + existing = schemas.enums[reference.class_name] + if values == existing.values: + break # This is the same Enum, we're good + dedup_counter += 1 + reference = Reference.from_ref(f"{reference.class_name}{dedup_counter}") + + for value in values.values(): + value_type = type(value) + break + else: + return PropertyError(data=data, detail="No values provided for Enum"), schemas + + default = None + if data.default is not None: + inverse_values = {v: k for k, v in values.items()} + try: + default = f"{reference.class_name}.{inverse_values[data.default]}" + except KeyError: + return ( + PropertyError( + detail=f"{data.default} is an invalid default for enum {reference.class_name}", data=data + ), + schemas, + ) + prop = EnumProperty( name=name, required=required, - values=EnumProperty.values_from_list(enum), - title=data.title or name, - default=data.default, + default=default, nullable=data.nullable, - existing_enums=schemas.enums, + reference=reference, + values=values, + value_type=value_type, ) - schemas = replace(schemas, enums={**schemas.enums, prop.reference.class_name: prop}) + schemas = attr.evolve(schemas, enums={**schemas.enums, prop.reference.class_name: prop}) return prop, schemas @@ -356,11 +313,13 @@ def build_union_property( if isinstance(sub_prop, PropertyError): return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), schemas sub_properties.append(sub_prop) + + default = convert_chain((prop._type_string for prop in sub_properties), data.default) return ( UnionProperty( name=name, required=required, - default=data.default, + default=default, inner_properties=sub_properties, nullable=data.nullable, ), @@ -380,7 +339,7 @@ def build_list_property( ListProperty( name=name, required=required, - default=data.default, + default=None, inner_property=inner_prop, nullable=data.nullable, ), @@ -398,29 +357,27 @@ def _property_from_data( name = utils.remove_string_escapes(name) if isinstance(data, oai.Reference): reference = Reference.from_ref(data.ref) - if reference.class_name in schemas.enums: - existing = schemas.enums[reference.class_name] + existing = schemas.enums.get(reference.class_name) or schemas.models.get(reference.class_name) + if existing: return ( - replace(existing, required=required, name=name, title=reference.class_name, existing_enums={}), + attr.evolve(existing, required=required, name=name), schemas, ) - elif reference.class_name in schemas.models: - return replace(schemas.models[reference.class_name], required=required, name=name), schemas - else: - return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas + return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas if data.enum: return build_enum_property(data=data, name=name, required=required, schemas=schemas, enum=data.enum) if data.anyOf or data.oneOf: return build_union_property(data=data, name=name, required=required, schemas=schemas) if not data.type: return PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined."), schemas + if data.type == "string": return _string_based_property(name=name, required=required, data=data), schemas elif data.type == "number": return ( FloatProperty( name=name, - default=data.default, + default=convert("float", data.default), required=required, nullable=data.nullable, ), @@ -430,7 +387,7 @@ def _property_from_data( return ( IntProperty( name=name, - default=data.default, + default=convert("int", data.default), required=required, nullable=data.nullable, ), @@ -441,7 +398,7 @@ def _property_from_data( BooleanProperty( name=name, required=required, - default=data.default, + default=convert("bool", data.default), nullable=data.nullable, ), schemas, diff --git a/openapi_python_client/parser/properties/converter.py b/openapi_python_client/parser/properties/converter.py new file mode 100644 index 000000000..b493755fc --- /dev/null +++ b/openapi_python_client/parser/properties/converter.py @@ -0,0 +1,82 @@ +""" Utils for converting default values into valid Python """ +__all__ = ["convert", "convert_chain"] + +from typing import Any, Callable, Dict, Iterable, Optional, TypeVar + +from dateutil.parser import isoparse + +from ... import utils +from ..errors import ValidationError + +T = TypeVar("T") + + +def convert(type_string: str, value: Any) -> Optional[Any]: + """ + Used by properties to convert some value into a valid value for the type_string. + + Args: + type_string: The string of the actual type that this default will be in the generated client. + value: The default value to try to convert. + + Returns: + The converted value if conversion was successful, or None of the value was None. + + Raises: + ValidationError if value could not be converted for type_string. + """ + if value is None: + return None + if type_string not in _CONVERTERS: + raise ValidationError() + try: + return _CONVERTERS[type_string](value) + except (KeyError, ValueError) as e: + raise ValidationError from e + + +def convert_chain(type_strings: Iterable[str], value: Any) -> Optional[Any]: + """ + Used by properties which support multiple possible converters (Unions). + + Args: + type_strings: Iterable of all the supported type_strings. + value: The default value to try to convert. + + Returns: + The converted value if conversion was successful, or None of the value was None. + + Raises: + ValidationError if value could not be converted for type_string. + """ + for type_string in type_strings: + try: + val = convert(type_string, value) + return val + except ValidationError: + continue + raise ValidationError() + + +def _convert_string(value: str) -> Optional[str]: + return f"{utils.remove_string_escapes(value)!r}" + + +def _convert_datetime(value: str) -> Optional[str]: + isoparse(value) # Make sure it works + return f"isoparse({value!r})" + + +def _convert_date(value: str) -> Optional[str]: + isoparse(value).date() + return f"isoparse({value!r}).date()" + + +_CONVERTERS: Dict[str, Callable[[Any], Optional[Any]]] = { + "str": _convert_string, + "datetime.datetime": _convert_datetime, + "datetime.date": _convert_date, + "float": float, + "int": int, + "bool": bool, +} diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index e60847764..be6311e42 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -1,47 +1,27 @@ __all__ = ["EnumProperty"] -from dataclasses import InitVar, dataclass, field -from typing import Any, ClassVar, Dict, List, Set, Type, Union +from typing import Any, ClassVar, Dict, List, Optional, Set, Type, Union + +import attr from ... import utils -from ..errors import ValidationError from ..reference import Reference from .property import Property ValueType = Union[str, int] -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class EnumProperty(Property): """ A property that should use an enum """ values: Dict[str, ValueType] - reference: Reference = field(init=False) - value_type: Type[ValueType] = field(init=False) - - title: InitVar[str] - existing_enums: InitVar[Dict[str, "EnumProperty"]] + reference: Reference + value_type: Type[ValueType] + default: Optional[Any] = attr.ib() template: ClassVar[str] = "enum_property.pyi" - def __post_init__(self, title: str, existing_enums: Dict[str, "EnumProperty"]) -> None: # type: ignore - reference = Reference.from_ref(title) - dedup_counter = 0 - while reference.class_name in existing_enums: - existing = existing_enums[reference.class_name] - if self.values == existing.values: - break # This is the same Enum, we're good - dedup_counter += 1 - reference = Reference.from_ref(f"{reference.class_name}{dedup_counter}") - - self.reference = reference - - for value in self.values.values(): - self.value_type = type(value) - break - - super().__post_init__() - def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ @@ -87,10 +67,3 @@ def values_from_list(values: List[ValueType]) -> Dict[str, ValueType]: sanitized_key = utils.snake_case(key).upper() output[sanitized_key] = utils.remove_string_escapes(value) return output - - def _validate_default(self, default: Any) -> str: - inverse_values = {v: k for k, v in self.values.items()} - try: - return f"{self.reference.class_name}.{inverse_values[default]}" - except KeyError as e: - raise ValidationError() from e diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index a5124b78c..eadfe30a7 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -1,11 +1,12 @@ -from dataclasses import dataclass from typing import ClassVar, List, Set +import attr + from ..reference import Reference from .property import Property -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class ModelProperty(Property): """ A property which refers to another Schema """ diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index f282c22ef..e5af34911 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -1,11 +1,11 @@ -from dataclasses import dataclass, field -from typing import Any, ClassVar, Optional, Set +from typing import ClassVar, Optional, Set + +import attr from ... import utils -from ..errors import ValidationError -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class Property: """ Describes a single property for a schema @@ -23,21 +23,14 @@ class Property: name: str required: bool nullable: bool - default: Optional[Any] + _type_string: ClassVar[str] = "" + default: Optional[str] = attr.ib() + python_name: str = attr.ib(init=False) template: ClassVar[Optional[str]] = None - _type_string: ClassVar[str] - - python_name: str = field(init=False) - - def __post_init__(self) -> None: - self.python_name = utils.to_valid_python_identifier(utils.snake_case(self.name)) - if self.default is not None: - self.default = self._validate_default(default=self.default) - def _validate_default(self, default: Any) -> Any: - """ Check that the default value is valid for the property's type + perform any necessary sanitization """ - raise ValidationError + def __attrs_post_init__(self) -> None: + object.__setattr__(self, "python_name", utils.to_valid_python_identifier(utils.snake_case(self.name))) def get_type_string(self, no_optional: bool = False) -> str: """ @@ -74,6 +67,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: def to_string(self) -> str: """ How this should be declared in a dataclass """ + default: Optional[str] if self.default is not None: default = self.default elif not self.required: diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index f6f3949f1..48acdf392 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -1,17 +1,18 @@ __all__ = ["Schemas"] -from dataclasses import dataclass, field from typing import Dict, List +import attr + from ..errors import ParseError from .enum_property import EnumProperty from .model_property import ModelProperty -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class Schemas: """ Structure for containing all defined, shareable, and resuabled schemas (attr classes and Enums) """ - enums: Dict[str, EnumProperty] = field(default_factory=dict) - models: Dict[str, ModelProperty] = field(default_factory=dict) - errors: List[ParseError] = field(default_factory=list) + enums: Dict[str, EnumProperty] = attr.ib(factory=dict) + models: Dict[str, ModelProperty] = attr.ib(factory=dict) + errors: List[ParseError] = attr.ib(factory=list) diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index c9f801583..6ff594ac5 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -2,41 +2,31 @@ import openapi_python_client.schema as oai from openapi_python_client.parser.errors import PropertyError, ValidationError +from openapi_python_client.parser.properties import BooleanProperty, FloatProperty, IntProperty MODULE_NAME = "openapi_python_client.parser.properties" class TestProperty: - def test___post_init(self, mocker): - from openapi_python_client.parser.properties import Property - - validate_default = mocker.patch(f"{MODULE_NAME}.Property._validate_default") - - Property(name="a name", required=True, default=None, nullable=False) - validate_default.assert_not_called() - - Property(name="a name", required=True, default="the default value", nullable=False) - validate_default.assert_called_with(default="the default value") - - def test_get_type_string(self): + def test_get_type_string(self, mocker): from openapi_python_client.parser.properties import Property + mocker.patch.object(Property, "_type_string", "TestType") p = Property(name="test", required=True, default=None, nullable=False) - p._type_string = "TestType" base_type_string = f"TestType" assert p.get_type_string() == base_type_string - p.nullable = True + p = Property(name="test", required=True, default=None, nullable=True) assert p.get_type_string() == f"Optional[{base_type_string}]" assert p.get_type_string(no_optional=True) == base_type_string - p.required = False + p = Property(name="test", required=False, default=None, nullable=True) assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" assert p.get_type_string(no_optional=True) == base_type_string - p.nullable = False + p = Property(name="test", required=False, default=None, nullable=False) assert p.get_type_string() == f"Union[Unset, {base_type_string}]" assert p.get_type_string(no_optional=True) == base_type_string @@ -44,19 +34,18 @@ def test_to_string(self, mocker): from openapi_python_client.parser.properties import Property name = "test" + get_type_string = mocker.patch.object(Property, "get_type_string") p = Property(name=name, required=True, default=None, nullable=False) - get_type_string = mocker.patch.object(p, "get_type_string") assert p.to_string() == f"{name}: {get_type_string()}" - p.required = False + p = Property(name=name, required=False, default=None, nullable=False) assert p.to_string() == f"{name}: {get_type_string()} = UNSET" - p.required = True - p.nullable = True + p = Property(name=name, required=True, default=None, nullable=False) assert p.to_string() == f"{name}: {get_type_string()}" - p.default = "TEST" + p = Property(name=name, required=True, default="TEST", nullable=False) assert p.to_string() == f"{name}: {get_type_string()} = TEST" def test_get_imports(self): @@ -65,28 +54,16 @@ def test_get_imports(self): p = Property(name="test", required=True, default=None, nullable=False) assert p.get_imports(prefix="") == set() - p.required = False + p = Property(name="test", required=False, default=None, nullable=False) assert p.get_imports(prefix="") == {"from types import UNSET, Unset", "from typing import Union"} - p.nullable = True + p = Property(name="test", required=False, default=None, nullable=True) assert p.get_imports(prefix="") == { "from types import UNSET, Unset", "from typing import Optional", "from typing import Union", } - def test__validate_default(self): - from openapi_python_client.parser.properties import Property - - # should be okay if default isn't specified - p = Property(name="a name", required=True, default=None, nullable=False) - - with pytest.raises(ValidationError): - p._validate_default("a default value") - - with pytest.raises(ValidationError): - Property(name="a name", required=True, default="", nullable=False) - class TestStringProperty: def test_get_type_string(self): @@ -98,21 +75,15 @@ def test_get_type_string(self): assert p.get_type_string() == base_type_string - p.nullable = True + p = StringProperty(name="test", required=True, default=None, nullable=True) assert p.get_type_string() == f"Optional[{base_type_string}]" - p.required = False + p = StringProperty(name="test", required=False, default=None, nullable=True) assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" - p.nullable = False + p = StringProperty(name="test", required=False, default=None, nullable=False) assert p.get_type_string() == f"Union[Unset, {base_type_string}]" - def test__validate_default(self): - from openapi_python_client.parser.properties import StringProperty - - p = StringProperty(name="a name", required=True, default="the default value", nullable=False) - assert p.default == "'the default value'" - class TestDateTimeProperty: def test_get_imports(self): @@ -125,7 +96,7 @@ def test_get_imports(self): "from dateutil.parser import isoparse", } - p.required = False + p = DateTimeProperty(name="test", required=False, default=None, nullable=False) assert p.get_imports(prefix="...") == { "import datetime", "from typing import cast", @@ -134,7 +105,7 @@ def test_get_imports(self): "from ...types import UNSET, Unset", } - p.nullable = True + p = DateTimeProperty(name="test", required=False, default=None, nullable=True) assert p.get_imports(prefix="...") == { "import datetime", "from typing import cast", @@ -144,15 +115,6 @@ def test_get_imports(self): "from ...types import UNSET, Unset", } - def test__validate_default(self): - from openapi_python_client.parser.properties import DateTimeProperty - - with pytest.raises(ValidationError): - DateTimeProperty(name="a name", required=True, default="not a datetime", nullable=False) - - p = DateTimeProperty(name="a name", required=True, default="2017-07-21T17:32:28Z", nullable=False) - assert p.default == "isoparse('2017-07-21T17:32:28Z')" - class TestDateProperty: def test_get_imports(self): @@ -165,7 +127,7 @@ def test_get_imports(self): "from dateutil.parser import isoparse", } - p.required = False + p = DateProperty(name="test", required=False, default=None, nullable=False) assert p.get_imports(prefix="...") == { "import datetime", "from typing import cast", @@ -174,7 +136,7 @@ def test_get_imports(self): "from ...types import UNSET, Unset", } - p.nullable = True + p = DateProperty(name="test", required=False, default=None, nullable=True) assert p.get_imports(prefix="...") == { "import datetime", "from typing import cast", @@ -184,15 +146,6 @@ def test_get_imports(self): "from ...types import UNSET, Unset", } - def test__validate_default(self): - from openapi_python_client.parser.properties import DateProperty - - with pytest.raises(ValidationError): - DateProperty(name="a name", required=True, default="not a date", nullable=False) - - p = DateProperty(name="a name", required=True, default="1010-10-10", nullable=False) - assert p.default == "isoparse('1010-10-10').date()" - class TestFileProperty: def test_get_imports(self): @@ -204,14 +157,14 @@ def test_get_imports(self): "from ...types import File", } - p.required = False + p = FileProperty(name="test", required=False, default=None, nullable=False) assert p.get_imports(prefix=prefix) == { "from ...types import File", "from typing import Union", "from ...types import UNSET, Unset", } - p.nullable = True + p = FileProperty(name="test", required=False, default=None, nullable=True) assert p.get_imports(prefix=prefix) == { "from ...types import File", "from typing import Union", @@ -219,54 +172,6 @@ def test_get_imports(self): "from ...types import UNSET, Unset", } - def test__validate_default(self): - from openapi_python_client.parser.properties import FileProperty - - # should be okay if default isn't specified - FileProperty(name="a name", required=True, default=None, nullable=False) - - with pytest.raises(ValidationError): - FileProperty(name="a name", required=True, default="", nullable=False) - - -class TestFloatProperty: - def test__validate_default(self): - from openapi_python_client.parser.properties import FloatProperty - - # should be okay if default isn't specified - FloatProperty(name="a name", required=True, default=None, nullable=False) - - p = FloatProperty(name="a name", required=True, default="123.123", nullable=False) - assert p.default == 123.123 - - with pytest.raises(ValidationError): - FloatProperty(name="a name", required=True, default="not a float", nullable=False) - - -class TestIntProperty: - def test__validate_default(self): - from openapi_python_client.parser.properties import IntProperty - - # should be okay if default isn't specified - IntProperty(name="a name", required=True, default=None, nullable=False) - - p = IntProperty(name="a name", required=True, default="123", nullable=False) - assert p.default == 123 - - with pytest.raises(ValidationError): - IntProperty(name="a name", required=True, default="not an int", nullable=False) - - -class TestBooleanProperty: - def test__validate_default(self): - from openapi_python_client.parser.properties import BooleanProperty - - # should be okay if default isn't specified - BooleanProperty(name="a name", required=True, default=None, nullable=False) - - p = BooleanProperty(name="a name", required=True, default="Literally anything will work", nullable=False) - assert p.default == True - class TestListProperty: def test_get_type_string(self, mocker): @@ -281,15 +186,15 @@ def test_get_type_string(self, mocker): assert p.get_type_string() == base_type_string - p.nullable = True + p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=True) assert p.get_type_string() == f"Optional[{base_type_string}]" assert p.get_type_string(no_optional=True) == base_type_string - p.required = False + p = ListProperty(name="test", required=False, default=None, inner_property=inner_property, nullable=True) assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" assert p.get_type_string(no_optional=True) == base_type_string - p.nullable = False + p = ListProperty(name="test", required=False, default=None, inner_property=inner_property, nullable=False) assert p.get_type_string() == f"Union[Unset, {base_type_string}]" assert p.get_type_string(no_optional=True) == base_type_string @@ -307,7 +212,7 @@ def test_get_type_imports(self, mocker): "from typing import List", } - p.required = False + p = ListProperty(name="test", required=False, default=None, inner_property=inner_property, nullable=False) assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", @@ -315,7 +220,7 @@ def test_get_type_imports(self, mocker): "from ...types import UNSET, Unset", } - p.nullable = True + p = ListProperty(name="test", required=False, default=None, inner_property=inner_property, nullable=True) assert p.get_imports(prefix=prefix) == { inner_import, "from typing import List", @@ -324,14 +229,6 @@ def test_get_type_imports(self, mocker): "from ...types import UNSET, Unset", } - def test__validate_default(self, mocker): - from openapi_python_client.parser.properties import ListProperty - - inner_property = mocker.MagicMock() - - p = ListProperty(name="a name", required=True, default=["x"], inner_property=inner_property, nullable=False) - assert p.default is None - class TestUnionProperty: def test_get_type_string(self, mocker): @@ -353,16 +250,34 @@ def test_get_type_string(self, mocker): assert p.get_type_string() == base_type_string - p.nullable = True + p = UnionProperty( + name="test", + required=True, + default=None, + inner_properties=[inner_property_1, inner_property_2], + nullable=True, + ) assert p.get_type_string() == f"Optional[{base_type_string}]" assert p.get_type_string(no_optional=True) == base_type_string base_type_string_with_unset = f"Union[Unset, inner_type_string_1, inner_type_string_2]" - p.required = False + p = UnionProperty( + name="test", + required=False, + default=None, + inner_properties=[inner_property_1, inner_property_2], + nullable=True, + ) assert p.get_type_string() == f"Optional[{base_type_string_with_unset}]" assert p.get_type_string(no_optional=True) == base_type_string - p.nullable = False + p = UnionProperty( + name="test", + required=False, + default=None, + inner_properties=[inner_property_1, inner_property_2], + nullable=False, + ) assert p.get_type_string() == base_type_string_with_unset assert p.get_type_string(no_optional=True) == base_type_string @@ -390,7 +305,13 @@ def test_get_type_imports(self, mocker): "from typing import Union", } - p.required = False + p = UnionProperty( + name="test", + required=False, + default=None, + inner_properties=[inner_property_1, inner_property_2], + nullable=False, + ) assert p.get_imports(prefix=prefix) == { inner_import_1, inner_import_2, @@ -398,7 +319,13 @@ def test_get_type_imports(self, mocker): "from ...types import UNSET, Unset", } - p.nullable = True + p = UnionProperty( + name="test", + required=False, + default=None, + inner_properties=[inner_property_1, inner_property_2], + nullable=True, + ) assert p.get_imports(prefix=prefix) == { inner_import_1, inner_import_2, @@ -407,147 +334,107 @@ def test_get_type_imports(self, mocker): "from ...types import UNSET, Unset", } - def test__validate_default(self, mocker): - from openapi_python_client.parser.properties import UnionProperty - - inner_property_1 = mocker.MagicMock() - inner_property_1.get_type_string.return_value = "inner_type_string_1" - inner_property_1._validate_default.side_effect = ValidationError() - inner_property_2 = mocker.MagicMock() - inner_property_2.get_type_string.return_value = "inner_type_string_2" - inner_property_2._validate_default.return_value = "the default value" - p = UnionProperty( - name="test", - required=True, - default="a value", - inner_properties=[inner_property_1, inner_property_2], - nullable=False, - ) - - assert p.default == "the default value" - - inner_property_2._validate_default.side_effect = ValidationError() - - with pytest.raises(ValidationError): - UnionProperty( - name="test", - required=True, - default="a value", - inner_properties=[inner_property_1, inner_property_2], - nullable=False, - ) - class TestEnumProperty: - def test___post_init__(self, mocker): - name = "test" - - fake_reference = mocker.MagicMock(class_name="MyTestEnum") - deduped_reference = mocker.MagicMock(class_name="Deduped") - from_ref = mocker.patch( - f"{MODULE_NAME}.Reference.from_ref", side_effect=[fake_reference, deduped_reference, deduped_reference] - ) - from openapi_python_client.parser import properties - - fake_dup_enum = mocker.MagicMock() - values = {"FIRST": "first", "SECOND": "second"} - - enum_property = properties.EnumProperty( - name=name, - required=True, - default="second", - values=values, - title="a_title", - nullable=False, - existing_enums={"MyTestEnum": fake_dup_enum}, - ) - - assert enum_property.default == "Deduped.SECOND" - assert enum_property.python_name == name - from_ref.assert_has_calls([mocker.call("a_title"), mocker.call("MyTestEnum1")]) - assert enum_property.reference == deduped_reference - - # Test encountering exactly the same Enum again - assert ( - properties.EnumProperty( - name=name, - required=True, - default="second", - values=values, - title="a_title", - nullable=False, - existing_enums={"MyTestEnum": fake_dup_enum, "Deduped": enum_property}, - ) - == enum_property - ) - - # What if an Enum exists with the same name, but has the same values? Don't dedupe that. - fake_dup_enum.values = values - from_ref.reset_mock() - from_ref.side_effect = [fake_reference] - enum_property = properties.EnumProperty( - name=name, - required=True, - default="second", - values=values, - title="a_title", - nullable=False, - existing_enums={"MyTestEnum": fake_dup_enum, "Deduped": enum_property}, - ) - assert enum_property.default == "MyTestEnum.SECOND" - assert enum_property.python_name == name - from_ref.assert_called_once_with("a_title") - assert enum_property.reference == fake_reference - def test_get_type_string(self, mocker): fake_reference = mocker.MagicMock(class_name="MyTestEnum") - mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference) from openapi_python_client.parser import properties p = properties.EnumProperty( - name="test", required=True, default=None, values={}, title="a_title", nullable=False, existing_enums={} + name="test", + required=True, + default=None, + values={}, + nullable=False, + reference=fake_reference, + value_type=str, ) base_type_string = f"MyTestEnum" assert p.get_type_string() == base_type_string - p.nullable = True + p = properties.EnumProperty( + name="test", + required=True, + default=None, + values={}, + nullable=True, + reference=fake_reference, + value_type=str, + ) assert p.get_type_string() == f"Optional[{base_type_string}]" assert p.get_type_string(no_optional=True) == base_type_string - p.required = False + p = properties.EnumProperty( + name="test", + required=False, + default=None, + values={}, + nullable=True, + reference=fake_reference, + value_type=str, + ) assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" assert p.get_type_string(no_optional=True) == base_type_string - p.nullable = False + p = properties.EnumProperty( + name="test", + required=False, + default=None, + values={}, + nullable=False, + reference=fake_reference, + value_type=str, + ) assert p.get_type_string() == f"Union[Unset, {base_type_string}]" assert p.get_type_string(no_optional=True) == base_type_string def test_get_imports(self, mocker): fake_reference = mocker.MagicMock(class_name="MyTestEnum", module_name="my_test_enum") - mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference) prefix = "..." from openapi_python_client.parser import properties enum_property = properties.EnumProperty( - name="test", required=True, default=None, values={}, title="a_title", nullable=False, existing_enums={} + name="test", + required=True, + default=None, + values={}, + nullable=False, + reference=fake_reference, + value_type=str, ) assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", } - enum_property.required = False + enum_property = properties.EnumProperty( + name="test", + required=False, + default=None, + values={}, + nullable=False, + reference=fake_reference, + value_type=str, + ) assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", "from typing import Union", "from ...types import UNSET, Unset", } - enum_property.nullable = True + enum_property = properties.EnumProperty( + name="test", + required=False, + default=None, + values={}, + nullable=True, + reference=fake_reference, + value_type=str, + ) assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}models.{fake_reference.module_name} import {fake_reference.class_name}", "from typing import Union", @@ -580,68 +467,68 @@ def test_values_from_list_duplicate(self): with pytest.raises(ValueError): EnumProperty.values_from_list(data) - def test__validate_default(self, mocker): - fake_reference = mocker.MagicMock(class_name="MyTestEnum", module_name="my_test_enum") - mocker.patch(f"{MODULE_NAME}.Reference.from_ref", return_value=fake_reference) - from openapi_python_client.parser import properties +class TestPropertyFromData: + def test_property_from_data_str_enum(self, mocker): + from openapi_python_client.parser.properties import EnumProperty, Reference + from openapi_python_client.schema import Schema - enum_property = properties.EnumProperty( - name="test", + data = Schema(title="AnEnum", enum=["A", "B", "C"], nullable=False, default="B") + name = "my_enum" + required = True + + from openapi_python_client.parser.properties import Schemas, property_from_data + + schemas = Schemas(enums={"AnEnum": mocker.MagicMock()}) + + prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) + + assert prop == EnumProperty( + name="my_enum", required=True, - default="test", - values={"TEST": "test"}, - title="a_title", nullable=False, - existing_enums={}, + values={"A": "A", "B": "B", "C": "C"}, + reference=Reference(class_name="AnEnum1", module_name="an_enum1"), + value_type=str, + default="AnEnum1.B", ) - assert enum_property.default == "MyTestEnum.TEST" - - with pytest.raises(ValidationError): - properties.EnumProperty( - name="test", - required=True, - default="bad_val", - values={"TEST": "test"}, - title="a_title", - nullable=False, - existing_enums={}, - ) + assert schemas != new_schemas, "Provided Schemas was mutated" + assert new_schemas.enums == { + "AnEnum": schemas.enums["AnEnum"], + "AnEnum1": prop, + } + def test_property_from_data_int_enum(self, mocker): + from openapi_python_client.parser.properties import EnumProperty, Reference + from openapi_python_client.schema import Schema -class TestPropertyFromData: - def test_property_from_data_enum(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = mocker.MagicMock(title=None) - EnumProperty = mocker.patch(f"{MODULE_NAME}.EnumProperty") - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + data = Schema.construct(title="AnEnum", enum=[1, 2, 3], nullable=False, default=3) + name = "my_enum" + required = True from openapi_python_client.parser.properties import Schemas, property_from_data - schemas = Schemas(enums={"blah": mocker.MagicMock()}) + schemas = Schemas(enums={"AnEnum": mocker.MagicMock()}) prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) - EnumProperty.values_from_list.assert_called_once_with(data.enum) - EnumProperty.assert_called_once_with( - name=name, - required=required, - values=EnumProperty.values_from_list(), - default=data.default, - title=name, - nullable=data.nullable, - existing_enums=schemas.enums, + assert prop == EnumProperty( + name="my_enum", + required=True, + nullable=False, + values={"VALUE_1": 1, "VALUE_2": 2, "VALUE_3": 3}, + reference=Reference(class_name="AnEnum1", module_name="an_enum1"), + value_type=int, + default="AnEnum1.VALUE_3", ) - assert prop == EnumProperty.return_value assert schemas != new_schemas, "Provided Schemas was mutated" assert new_schemas.enums == { - "blah": new_schemas.enums["blah"], - EnumProperty.return_value.reference.class_name: EnumProperty.return_value, + "AnEnum": schemas.enums["AnEnum"], + "AnEnum1": prop, } - def test_property_from_data_ref_enum(self, mocker): - from openapi_python_client.parser.properties import EnumProperty, Schemas, property_from_data + def test_property_from_data_ref_enum(self): + from openapi_python_client.parser.properties import EnumProperty, Reference, Schemas, property_from_data name = "some_enum" data = oai.Reference.construct(ref="MyEnum") @@ -651,8 +538,8 @@ def test_property_from_data_ref_enum(self, mocker): nullable=False, default=None, values={"A": "a"}, - title="MyEnum", - existing_enums={}, + value_type=str, + reference=Reference(class_name="MyEnum", module_name="my_enum"), ) schemas = Schemas(enums={"MyEnum": existing_enum}) @@ -664,12 +551,12 @@ def test_property_from_data_ref_enum(self, mocker): nullable=False, default=None, values={"A": "a"}, - title="MyEnum", - existing_enums={}, + value_type=str, + reference=Reference(class_name="MyEnum", module_name="my_enum"), ) assert schemas == new_schemas - def test_property_from_data_ref_model(self, mocker): + def test_property_from_data_ref_model(self): from openapi_python_client.parser.properties import ModelProperty, Reference, Schemas, property_from_data name = "new_name" @@ -737,41 +624,47 @@ def test_property_from_data_string(self, mocker): _string_based_property.assert_called_once_with(name=name, required=required, data=data) @pytest.mark.parametrize( - "openapi_type,python_type", + "openapi_type,prop_type,python_type", [ - ("number", "FloatProperty"), - ("integer", "IntProperty"), - ("boolean", "BooleanProperty"), + ("number", FloatProperty, float), + ("integer", IntProperty, int), + ("boolean", BooleanProperty, bool), ], ) - def test_property_from_data_simple_types(self, mocker, openapi_type, python_type): + def test_property_from_data_simple_types(self, openapi_type, prop_type, python_type): from openapi_python_client.parser.properties import Schemas, property_from_data - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Schema.construct(type=openapi_type) - clazz = mocker.patch(f"{MODULE_NAME}.{python_type}") - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + name = "test_prop" + required = True + data = oai.Schema.construct(type=openapi_type, default=1) schemas = Schemas() p, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) - clazz.assert_called_once_with(name=name, required=required, default=None, nullable=False) - assert p == clazz.return_value + assert p == prop_type(name=name, required=required, default=python_type(data.default), nullable=False) assert new_schemas == schemas - # Test optional values - clazz.reset_mock() - data.default = mocker.MagicMock() - data.nullable = mocker.MagicMock() + # Test nullable values + data.default = 0 + data.nullable = True - property_from_data( + p, _ = property_from_data( name=name, required=required, data=data, schemas=schemas, ) - clazz.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable) + assert p == prop_type(name=name, required=required, default=python_type(data.default), nullable=True) + + # Test bad default value + data.default = "a" + p, _ = property_from_data( + name=name, + required=required, + data=data, + schemas=schemas, + ) + assert python_type is bool or isinstance(p, PropertyError) def test_property_from_data_array(self, mocker): from openapi_python_client.parser.properties import Schemas, property_from_data @@ -932,8 +825,8 @@ def test_property_from_data_union(self, mocker): p, s = property_from_data(name=name, required=required, data=data, schemas=Schemas()) - FloatProperty.assert_called_once_with(name=name, required=required, default="0.0", nullable=False) - IntProperty.assert_called_once_with(name=name, required=required, default="0", nullable=False) + FloatProperty.assert_called_once_with(name=name, required=required, default=0.0, nullable=False) + IntProperty.assert_called_once_with(name=name, required=required, default=0, nullable=False) UnionProperty.assert_called_once_with( name=name, required=required, @@ -958,136 +851,87 @@ def test_property_from_data_union_bad_type(self, mocker): class TestStringBasedProperty: - def test__string_based_property_no_format(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Schema.construct(type="string", nullable=mocker.MagicMock()) - StringProperty = mocker.patch(f"{MODULE_NAME}.StringProperty") + def test__string_based_property_no_format(self): + from openapi_python_client.parser.properties import StringProperty + + name = "some_prop" + required = True + data = oai.Schema.construct(type="string", nullable=True, default='"hello world"') from openapi_python_client.parser.properties import _string_based_property p = _string_based_property(name=name, required=required, data=data) - StringProperty.assert_called_once_with( - name=name, required=required, pattern=None, default=None, nullable=data.nullable - ) - assert p == StringProperty.return_value + assert p == StringProperty(name=name, required=required, nullable=True, default="'\\\\\"hello world\\\\\"'") - # Test optional values - StringProperty.reset_mock() - data.default = mocker.MagicMock() - data.pattern = mocker.MagicMock() + data.pattern = "abcdef" + data.nullable = False - _string_based_property( + p = _string_based_property( name=name, required=required, data=data, ) - StringProperty.assert_called_once_with( - name=name, required=required, pattern=data.pattern, default=data.default, nullable=data.nullable + assert p == StringProperty( + name=name, required=required, nullable=False, default="'\\\\\"hello world\\\\\"'", pattern="abcdef" ) - def test__string_based_property_datetime_format(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Schema.construct(type="string", schema_format="date-time", nullable=mocker.MagicMock()) - DateTimeProperty = mocker.patch(f"{MODULE_NAME}.DateTimeProperty") + def test__string_based_property_datetime_format(self): + from openapi_python_client.parser.properties import DateTimeProperty, _string_based_property - from openapi_python_client.parser.properties import _string_based_property + name = "datetime_prop" + required = True + data = oai.Schema.construct( + type="string", schema_format="date-time", nullable=True, default="2020-11-06T12:00:00" + ) p = _string_based_property(name=name, required=required, data=data) - DateTimeProperty.assert_called_once_with(name=name, required=required, default=None, nullable=data.nullable) - assert p == DateTimeProperty.return_value - - # Test optional values - DateTimeProperty.reset_mock() - data.default = mocker.MagicMock() - - _string_based_property( - name=name, - required=required, - data=data, - ) - DateTimeProperty.assert_called_once_with( - name=name, required=required, default=data.default, nullable=data.nullable + assert p == DateTimeProperty( + name=name, required=required, nullable=True, default="isoparse('2020-11-06T12:00:00')" ) - def test__string_based_property_date_format(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Schema.construct(type="string", schema_format="date", nullable=mocker.MagicMock()) - DateProperty = mocker.patch(f"{MODULE_NAME}.DateProperty") + # Test bad default + data.default = "a" + with pytest.raises(ValidationError): + _string_based_property(name=name, required=required, data=data) - from openapi_python_client.parser.properties import _string_based_property + def test__string_based_property_date_format(self): + from openapi_python_client.parser.properties import DateProperty, _string_based_property + + name = "date_prop" + required = True + data = oai.Schema.construct(type="string", schema_format="date", nullable=True, default="2020-11-06") p = _string_based_property(name=name, required=required, data=data) - DateProperty.assert_called_once_with(name=name, required=required, default=None, nullable=data.nullable) - assert p == DateProperty.return_value - # Test optional values - DateProperty.reset_mock() - data.default = mocker.MagicMock() + assert p == DateProperty(name=name, required=required, nullable=True, default="isoparse('2020-11-06').date()") - _string_based_property( - name=name, - required=required, - data=data, - ) - DateProperty.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable) + # Test bad default + data.default = "a" + with pytest.raises(ValidationError): + _string_based_property(name=name, required=required, data=data) - def test__string_based_property_binary_format(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Schema.construct(type="string", schema_format="binary", nullable=mocker.MagicMock()) - FileProperty = mocker.patch(f"{MODULE_NAME}.FileProperty") + def test__string_based_property_binary_format(self): + from openapi_python_client.parser.properties import FileProperty, _string_based_property - from openapi_python_client.parser.properties import _string_based_property + name = "file_prop" + required = True + data = oai.Schema.construct(type="string", schema_format="binary", nullable=True, default="a") p = _string_based_property(name=name, required=required, data=data) - FileProperty.assert_called_once_with(name=name, required=required, default=None, nullable=data.nullable) - assert p == FileProperty.return_value - - # Test optional values - FileProperty.reset_mock() - data.default = mocker.MagicMock() - - _string_based_property( - name=name, - required=required, - data=data, - ) - FileProperty.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable) + assert p == FileProperty(name=name, required=required, nullable=True, default=None) def test__string_based_property_unsupported_format(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Schema.construct(type="string", schema_format=mocker.MagicMock()) - data.nullable = mocker.MagicMock() - StringProperty = mocker.patch(f"{MODULE_NAME}.StringProperty") + from openapi_python_client.parser.properties import StringProperty, _string_based_property - from openapi_python_client.parser.properties import _string_based_property + name = "unknown" + required = True + data = oai.Schema.construct(type="string", schema_format="blah", nullable=True) p = _string_based_property(name=name, required=required, data=data) - StringProperty.assert_called_once_with( - name=name, required=required, pattern=None, default=None, nullable=data.nullable - ) - assert p == StringProperty.return_value - - # Test optional values - StringProperty.reset_mock() - data.default = mocker.MagicMock() - data.pattern = mocker.MagicMock() - - _string_based_property( - name=name, - required=required, - data=data, - ) - StringProperty.assert_called_once_with( - name=name, required=required, pattern=data.pattern, default=data.default, nullable=data.nullable - ) + assert p == StringProperty(name=name, required=required, nullable=True, default=None) # def test_model_from_data(mocker): From 4cc04b3732728ef0f754f6ee742a9f74e7a882d1 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Fri, 6 Nov 2020 16:47:17 -0700 Subject: [PATCH 16/25] Fix forward references and properly allow Unset for ModelPropertys --- .../api/tests/defaults_tests_defaults_post.py | 7 +- .../tests/json_body_tests_json_body_post.py | 44 +++++++ .../api/tests/test_inline_objects.py | 8 +- .../my_test_api_client/models/__init__.py | 3 +- .../my_test_api_client/models/a_model.py | 123 ++++++++++++++++++ .../my_test_api_client/models/dict_prop.py | 18 --- .../models/http_validation_error.py | 40 ++++++ .../api/tests/defaults_tests_defaults_post.py | 13 -- .../tests/json_body_tests_json_body_post.py | 106 +++++++++++++++ .../api/tests/test_inline_objects.py | 10 +- .../my_test_api_client/models/__init__.py | 3 +- .../my_test_api_client/models/a_model.py | 123 ++++++++++++++++++ .../my_test_api_client/models/dict_prop.py | 18 --- .../models/http_validation_error.py | 40 ++++++ end_to_end_tests/openapi.json | 26 +--- .../parser/properties/__init__.py | 45 +++++-- .../parser/properties/model_property.py | 11 +- 17 files changed, 534 insertions(+), 104 deletions(-) create mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/json_body_tests_json_body_post.py create mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py delete mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/models/dict_prop.py create mode 100644 end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/a_model.py delete mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py index e262edbd7..fb6cf0f1f 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -5,12 +5,11 @@ Client = httpx.Client import datetime -from typing import Dict, List, Union, cast +from typing import List, Union, cast from dateutil.parser import isoparse from ...models.an_enum import AnEnum -from ...models.dict_prop import DictProp from ...models.http_validation_error import HTTPValidationError from ...types import UNSET, Unset @@ -35,7 +34,6 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, H def httpx_request( *, client: Client, - json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -95,12 +93,9 @@ def httpx_request( if enum_prop is not UNSET: params["enum_prop"] = json_enum_prop - json_json_body = json_body.to_dict() - response = client.request( "post", "/tests/defaults", - json=json_json_body, params=params, ) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/json_body_tests_json_body_post.py new file mode 100644 index 000000000..76f4ffe84 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/json_body_tests_json_body_post.py @@ -0,0 +1,44 @@ +from typing import Optional + +import httpx + +Client = httpx.Client + +from typing import Dict, cast + +from ...models.a_model import AModel +from ...models.http_validation_error import HTTPValidationError + + +def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: + if response.status_code == 200: + return None + if response.status_code == 422: + return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + return None + + +def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, HTTPValidationError]]: + return httpx.Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=_parse_response(response=response), + ) + + +def httpx_request( + *, + client: Client, + json_body: AModel, +) -> httpx.Response[Union[None, HTTPValidationError]]: + + json_json_body = json_body.to_dict() + + response = client.request( + "post", + "/tests/json_body", + json=json_json_body, + ) + + return _build_response(response=response) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py index 9d044a0fa..bd9bb9fff 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py @@ -1,9 +1,9 @@ -from typing import Optional - import httpx Client = httpx.Client +from typing import Dict, Union + from ...models.json_body import JsonBody from ...types import UNSET, Unset @@ -20,10 +20,10 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[None]: def httpx_request( *, client: Client, - json_body: Optional[JsonBody], + json_body: Union[JsonBody, Unset], ) -> httpx.Response[None]: - json_json_body: Optional[JsonBody] = UNSET + json_json_body: Dict[str, Any] = UNSET if not isinstance(json_body, Unset): json_json_body = json_body.to_dict() diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/__init__.py index 49e4332bf..210b8b01d 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/__init__.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/__init__.py @@ -1,9 +1,10 @@ """ Contains all the data models used in inputs/outputs """ +from .a_model import AModel from .an_enum import AnEnum from .an_int_enum import AnIntEnum from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost -from .dict_prop import DictProp from .different_enum import DifferentEnum +from .http_validation_error import HTTPValidationError from .json_body import JsonBody from .validation_error import ValidationError diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py new file mode 100644 index 000000000..87175124c --- /dev/null +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py @@ -0,0 +1,123 @@ +import datetime +from typing import Any, Dict, List, Optional, Union + +import attr +from dateutil.parser import isoparse + +from ..models.an_enum import AnEnum +from ..models.different_enum import DifferentEnum +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class AModel: + """ A Model for testing all the ways custom objects can be used """ + + an_enum_value: AnEnum + a_camel_date_time: Union[datetime.datetime, datetime.date] + a_date: datetime.date + required_not_nullable: str + required_nullable: Optional[str] + nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET + attr_1_leading_digit: Union[Unset, str] = UNSET + not_required_nullable: Union[Unset, Optional[str]] = UNSET + not_required_not_nullable: Union[Unset, str] = UNSET + + def to_dict(self) -> Dict[str, Any]: + an_enum_value = self.an_enum_value.value + + if isinstance(self.a_camel_date_time, datetime.datetime): + a_camel_date_time = self.a_camel_date_time.isoformat() + + else: + a_camel_date_time = self.a_camel_date_time.isoformat() + + a_date = self.a_date.isoformat() + + required_not_nullable = self.required_not_nullable + nested_list_of_enums: Union[Unset, List[Any]] = UNSET + if not isinstance(self.nested_list_of_enums, Unset): + nested_list_of_enums = [] + for nested_list_of_enums_item_data in self.nested_list_of_enums: + nested_list_of_enums_item = [] + for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: + nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value + + nested_list_of_enums_item.append(nested_list_of_enums_item_item) + + nested_list_of_enums.append(nested_list_of_enums_item) + + attr_1_leading_digit = self.attr_1_leading_digit + required_nullable = self.required_nullable + not_required_nullable = self.not_required_nullable + not_required_not_nullable = self.not_required_not_nullable + + field_dict = { + "an_enum_value": an_enum_value, + "aCamelDateTime": a_camel_date_time, + "a_date": a_date, + "required_not_nullable": required_not_nullable, + "required_nullable": required_nullable, + } + if nested_list_of_enums is not UNSET: + field_dict["nested_list_of_enums"] = nested_list_of_enums + if attr_1_leading_digit is not UNSET: + field_dict["1_leading_digit"] = attr_1_leading_digit + if not_required_nullable is not UNSET: + field_dict["not_required_nullable"] = not_required_nullable + if not_required_not_nullable is not UNSET: + field_dict["not_required_not_nullable"] = not_required_not_nullable + + return field_dict + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "AModel": + an_enum_value = AnEnum(d["an_enum_value"]) + + def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]: + a_camel_date_time: Union[datetime.datetime, datetime.date] + try: + a_camel_date_time = isoparse(d["aCamelDateTime"]) + + return a_camel_date_time + except: # noqa: E722 + pass + a_camel_date_time = isoparse(d["aCamelDateTime"]).date() + + return a_camel_date_time + + a_camel_date_time = _parse_a_camel_date_time(d["aCamelDateTime"]) + + a_date = isoparse(d["a_date"]).date() + + required_not_nullable = d["required_not_nullable"] + + nested_list_of_enums = [] + for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: + nested_list_of_enums_item = [] + for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: + nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) + + nested_list_of_enums_item.append(nested_list_of_enums_item_item) + + nested_list_of_enums.append(nested_list_of_enums_item) + + attr_1_leading_digit = d.get("1_leading_digit", UNSET) + + required_nullable = d["required_nullable"] + + not_required_nullable = d.get("not_required_nullable", UNSET) + + not_required_not_nullable = d.get("not_required_not_nullable", UNSET) + + return AModel( + an_enum_value=an_enum_value, + a_camel_date_time=a_camel_date_time, + a_date=a_date, + required_not_nullable=required_not_nullable, + nested_list_of_enums=nested_list_of_enums, + attr_1_leading_digit=attr_1_leading_digit, + required_nullable=required_nullable, + not_required_nullable=not_required_nullable, + not_required_not_nullable=not_required_not_nullable, + ) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/dict_prop.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/dict_prop.py deleted file mode 100644 index 39f4a0123..000000000 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/dict_prop.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Any, Dict - -import attr - - -@attr.s(auto_attribs=True) -class DictProp: - """ """ - - def to_dict(self) -> Dict[str, Any]: - - field_dict = {} - - return field_dict - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "DictProp": - return DictProp() diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py new file mode 100644 index 000000000..9d29faa4d --- /dev/null +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py @@ -0,0 +1,40 @@ +from typing import Any, Dict, List, Union + +import attr + +from ..models.validation_error import ValidationError +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class HTTPValidationError: + """ """ + + detail: Union[Unset, List[ValidationError]] = UNSET + + def to_dict(self) -> Dict[str, Any]: + detail: Union[Unset, List[Any]] = UNSET + if not isinstance(self.detail, Unset): + detail = [] + for detail_item_data in self.detail: + detail_item = detail_item_data.to_dict() + + detail.append(detail_item) + + field_dict = {} + if detail is not UNSET: + field_dict["detail"] = detail + + return field_dict + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": + detail = [] + for detail_item_data in d.get("detail", UNSET) or []: + detail_item = ValidationError.from_dict(detail_item_data) + + detail.append(detail_item) + + return HTTPValidationError( + detail=detail, + ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 4d87ca628..77da9d588 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -6,7 +6,6 @@ from ...client import Client from ...models.an_enum import AnEnum -from ...models.dict_prop import DictProp from ...models.http_validation_error import HTTPValidationError from ...types import UNSET, Response, Unset @@ -14,7 +13,6 @@ def _get_kwargs( *, client: Client, - json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -77,14 +75,11 @@ def _get_kwargs( if enum_prop is not UNSET: params["enum_prop"] = json_enum_prop - json_json_body = json_body.to_dict() - return { "url": url, "headers": headers, "cookies": client.get_cookies(), "timeout": client.get_timeout(), - "json": json_json_body, "params": params, } @@ -109,7 +104,6 @@ def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPVal def sync_detailed( *, client: Client, - json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -122,7 +116,6 @@ def sync_detailed( ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, - json_body=json_body, string_prop=string_prop, datetime_prop=datetime_prop, date_prop=date_prop, @@ -144,7 +137,6 @@ def sync_detailed( def sync( *, client: Client, - json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -159,7 +151,6 @@ def sync( return sync_detailed( client=client, - json_body=json_body, string_prop=string_prop, datetime_prop=datetime_prop, date_prop=date_prop, @@ -175,7 +166,6 @@ def sync( async def asyncio_detailed( *, client: Client, - json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -188,7 +178,6 @@ async def asyncio_detailed( ) -> Response[Union[None, HTTPValidationError]]: kwargs = _get_kwargs( client=client, - json_body=json_body, string_prop=string_prop, datetime_prop=datetime_prop, date_prop=date_prop, @@ -209,7 +198,6 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - json_body: DictProp, string_prop: Union[Unset, str] = "the default string", datetime_prop: Union[Unset, datetime.datetime] = isoparse("1010-10-10T00:00:00"), date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), @@ -225,7 +213,6 @@ async def asyncio( return ( await asyncio_detailed( client=client, - json_body=json_body, string_prop=string_prop, datetime_prop=datetime_prop, date_prop=date_prop, diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py new file mode 100644 index 000000000..eb556c5d7 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py @@ -0,0 +1,106 @@ +from typing import Any, Dict, Optional, Union, cast + +import httpx + +from ...client import Client +from ...models.a_model import AModel +from ...models.http_validation_error import HTTPValidationError +from ...types import Response + + +def _get_kwargs( + *, + client: Client, + json_body: AModel, +) -> Dict[str, Any]: + url = "{}/tests/json_body".format(client.base_url) + + headers: Dict[str, Any] = client.get_headers() + + json_json_body = json_body.to_dict() + + return { + "url": url, + "headers": headers, + "cookies": client.get_cookies(), + "timeout": client.get_timeout(), + "json": json_json_body, + } + + +def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: + if response.status_code == 200: + return None + if response.status_code == 422: + return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + return None + + +def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPValidationError]]: + return Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=_parse_response(response=response), + ) + + +def sync_detailed( + *, + client: Client, + json_body: AModel, +) -> Response[Union[None, HTTPValidationError]]: + kwargs = _get_kwargs( + client=client, + json_body=json_body, + ) + + response = httpx.post( + **kwargs, + ) + + return _build_response(response=response) + + +def sync( + *, + client: Client, + json_body: AModel, +) -> Optional[Union[None, HTTPValidationError]]: + """ Try sending a JSON body """ + + return sync_detailed( + client=client, + json_body=json_body, + ).parsed + + +async def asyncio_detailed( + *, + client: Client, + json_body: AModel, +) -> Response[Union[None, HTTPValidationError]]: + kwargs = _get_kwargs( + client=client, + json_body=json_body, + ) + + async with httpx.AsyncClient() as _client: + response = await _client.post(**kwargs) + + return _build_response(response=response) + + +async def asyncio( + *, + client: Client, + json_body: AModel, +) -> Optional[Union[None, HTTPValidationError]]: + """ Try sending a JSON body """ + + return ( + await asyncio_detailed( + client=client, + json_body=json_body, + ) + ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py index a1824178a..8374c90fa 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict, Union import httpx @@ -10,13 +10,13 @@ def _get_kwargs( *, client: Client, - json_body: Optional[JsonBody], + json_body: Union[JsonBody, Unset], ) -> Dict[str, Any]: url = "{}/tests/inline_objects".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - json_json_body: Optional[JsonBody] = UNSET + json_json_body: Dict[str, Any] = UNSET if not isinstance(json_body, Unset): json_json_body = json_body.to_dict() @@ -41,7 +41,7 @@ def _build_response(*, response: httpx.Response) -> Response[None]: def sync_detailed( *, client: Client, - json_body: Optional[JsonBody], + json_body: Union[JsonBody, Unset], ) -> Response[None]: kwargs = _get_kwargs( client=client, @@ -58,7 +58,7 @@ def sync_detailed( async def asyncio_detailed( *, client: Client, - json_body: Optional[JsonBody], + json_body: Union[JsonBody, Unset], ) -> Response[None]: kwargs = _get_kwargs( client=client, 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 49e4332bf..210b8b01d 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 @@ -1,9 +1,10 @@ """ Contains all the data models used in inputs/outputs """ +from .a_model import AModel from .an_enum import AnEnum from .an_int_enum import AnIntEnum from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost -from .dict_prop import DictProp from .different_enum import DifferentEnum +from .http_validation_error import HTTPValidationError from .json_body import JsonBody from .validation_error import ValidationError diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py new file mode 100644 index 000000000..87175124c --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -0,0 +1,123 @@ +import datetime +from typing import Any, Dict, List, Optional, Union + +import attr +from dateutil.parser import isoparse + +from ..models.an_enum import AnEnum +from ..models.different_enum import DifferentEnum +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class AModel: + """ A Model for testing all the ways custom objects can be used """ + + an_enum_value: AnEnum + a_camel_date_time: Union[datetime.datetime, datetime.date] + a_date: datetime.date + required_not_nullable: str + required_nullable: Optional[str] + nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET + attr_1_leading_digit: Union[Unset, str] = UNSET + not_required_nullable: Union[Unset, Optional[str]] = UNSET + not_required_not_nullable: Union[Unset, str] = UNSET + + def to_dict(self) -> Dict[str, Any]: + an_enum_value = self.an_enum_value.value + + if isinstance(self.a_camel_date_time, datetime.datetime): + a_camel_date_time = self.a_camel_date_time.isoformat() + + else: + a_camel_date_time = self.a_camel_date_time.isoformat() + + a_date = self.a_date.isoformat() + + required_not_nullable = self.required_not_nullable + nested_list_of_enums: Union[Unset, List[Any]] = UNSET + if not isinstance(self.nested_list_of_enums, Unset): + nested_list_of_enums = [] + for nested_list_of_enums_item_data in self.nested_list_of_enums: + nested_list_of_enums_item = [] + for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: + nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value + + nested_list_of_enums_item.append(nested_list_of_enums_item_item) + + nested_list_of_enums.append(nested_list_of_enums_item) + + attr_1_leading_digit = self.attr_1_leading_digit + required_nullable = self.required_nullable + not_required_nullable = self.not_required_nullable + not_required_not_nullable = self.not_required_not_nullable + + field_dict = { + "an_enum_value": an_enum_value, + "aCamelDateTime": a_camel_date_time, + "a_date": a_date, + "required_not_nullable": required_not_nullable, + "required_nullable": required_nullable, + } + if nested_list_of_enums is not UNSET: + field_dict["nested_list_of_enums"] = nested_list_of_enums + if attr_1_leading_digit is not UNSET: + field_dict["1_leading_digit"] = attr_1_leading_digit + if not_required_nullable is not UNSET: + field_dict["not_required_nullable"] = not_required_nullable + if not_required_not_nullable is not UNSET: + field_dict["not_required_not_nullable"] = not_required_not_nullable + + return field_dict + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "AModel": + an_enum_value = AnEnum(d["an_enum_value"]) + + def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, datetime.date]: + a_camel_date_time: Union[datetime.datetime, datetime.date] + try: + a_camel_date_time = isoparse(d["aCamelDateTime"]) + + return a_camel_date_time + except: # noqa: E722 + pass + a_camel_date_time = isoparse(d["aCamelDateTime"]).date() + + return a_camel_date_time + + a_camel_date_time = _parse_a_camel_date_time(d["aCamelDateTime"]) + + a_date = isoparse(d["a_date"]).date() + + required_not_nullable = d["required_not_nullable"] + + nested_list_of_enums = [] + for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: + nested_list_of_enums_item = [] + for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: + nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) + + nested_list_of_enums_item.append(nested_list_of_enums_item_item) + + nested_list_of_enums.append(nested_list_of_enums_item) + + attr_1_leading_digit = d.get("1_leading_digit", UNSET) + + required_nullable = d["required_nullable"] + + not_required_nullable = d.get("not_required_nullable", UNSET) + + not_required_not_nullable = d.get("not_required_not_nullable", UNSET) + + return AModel( + an_enum_value=an_enum_value, + a_camel_date_time=a_camel_date_time, + a_date=a_date, + required_not_nullable=required_not_nullable, + nested_list_of_enums=nested_list_of_enums, + attr_1_leading_digit=attr_1_leading_digit, + required_nullable=required_nullable, + not_required_nullable=not_required_nullable, + not_required_not_nullable=not_required_not_nullable, + ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py b/end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py deleted file mode 100644 index 39f4a0123..000000000 --- a/end_to_end_tests/golden-record/my_test_api_client/models/dict_prop.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Any, Dict - -import attr - - -@attr.s(auto_attribs=True) -class DictProp: - """ """ - - def to_dict(self) -> Dict[str, Any]: - - field_dict = {} - - return field_dict - - @staticmethod - def from_dict(d: Dict[str, Any]) -> "DictProp": - return DictProp() diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py new file mode 100644 index 000000000..9d29faa4d --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py @@ -0,0 +1,40 @@ +from typing import Any, Dict, List, Union + +import attr + +from ..models.validation_error import ValidationError +from ..types import UNSET, Unset + + +@attr.s(auto_attribs=True) +class HTTPValidationError: + """ """ + + detail: Union[Unset, List[ValidationError]] = UNSET + + def to_dict(self) -> Dict[str, Any]: + detail: Union[Unset, List[Any]] = UNSET + if not isinstance(self.detail, Unset): + detail = [] + for detail_item_data in self.detail: + detail_item = detail_item_data.to_dict() + + detail.append(detail_item) + + field_dict = {} + if detail is not UNSET: + field_dict["detail"] = detail + + return field_dict + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": + detail = [] + for detail_item_data in d.get("detail", UNSET) or []: + detail_item = ValidationError.from_dict(detail_item_data) + + detail.append(detail_item) + + return HTTPValidationError( + detail=detail, + ) diff --git a/end_to_end_tests/openapi.json b/end_to_end_tests/openapi.json index f28f5a4e5..f3e1a26ae 100644 --- a/end_to_end_tests/openapi.json +++ b/end_to_end_tests/openapi.json @@ -381,22 +381,6 @@ "in": "query" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "title": "Dict Prop", - "type": "object", - "additionalProperties": { - "type": "string" - }, - "default": { - "key": "val" - } - } - } - } - }, "responses": { "200": { "description": "Successful Response", @@ -619,7 +603,7 @@ "schemas": { "AModel": { "title": "AModel", - "required": ["an_enum_value", "some_dict", "aCamelDateTime", "a_date", "required_nullable", "required_not_nullable"], + "required": ["an_enum_value", "aCamelDateTime", "a_date", "required_nullable", "required_not_nullable"], "type": "object", "properties": { "an_enum_value": { @@ -636,14 +620,6 @@ }, "default": [] }, - "some_dict": { - "title": "Some Dict", - "type": "object", - "additionalProperties": { - "type": "string" - }, - "nullable": true - }, "aCamelDateTime": { "title": "Acameldatetime", "anyOf": [ diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 5c5717864..833ad4766 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -422,17 +422,42 @@ def property_from_data( return PropertyError(detail="Failed to validate default value", data=data), schemas +def update_schemas_with_data(name: str, data: oai.Schema, schemas: Schemas) -> Union[Schemas, PropertyError]: + if data.enum is not None: + prop, schemas = build_enum_property(data=data, name=name, required=True, schemas=schemas, enum=data.enum) + else: + prop, schemas = build_model_property(data=data, name=name, schemas=schemas, required=True) + if isinstance(prop, PropertyError): + return prop + else: + return schemas + + def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) -> Schemas: """ Get a list of Schemas from an OpenAPI dict """ schemas = Schemas() - for name, data in components.items(): - if isinstance(data, oai.Reference): - schemas.errors.append(PropertyError(data=data, detail="Reference schemas are not supported.")) - continue - if data.enum is not None: - prop, schemas = build_enum_property(data=data, name=name, required=True, schemas=schemas, enum=data.enum) - continue - model, schemas = build_model_property(data=data, name=name, schemas=schemas, required=True) - if isinstance(model, PropertyError): - schemas.errors.append(model) + to_process = components.items() + processing = True + errors = [] + + # References could have forward References so keep going as long as we are making progress + while processing: + processing = False + errors = [] + next_round = [] + # Only accumulate errors from the last round, since we might fix some along the way + for name, data in to_process: + if isinstance(data, oai.Reference): + schemas.errors.append(PropertyError(data=data, detail="Reference schemas are not supported.")) + continue + schemas_or_err = update_schemas_with_data(name, data, schemas) + if isinstance(schemas_or_err, PropertyError): + next_round.append((name, data)) + errors.append(schemas_or_err) + else: + schemas = schemas_or_err + processing = True # We made some progress this round, do another after it's done + to_process = next_round + schemas.errors.extend(errors) + return schemas diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index eadfe30a7..021f4677a 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -22,9 +22,14 @@ class ModelProperty(Property): def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if no_optional or (self.required and not self.nullable): - return self.reference.class_name - return f"Optional[{self.reference.class_name}]" + type_string = self.reference.class_name + if no_optional: + return type_string + if self.nullable: + type_string = f"Optional[{type_string}]" + if not self.required: + type_string = f"Union[{type_string}, Unset]" + return type_string def get_imports(self, *, prefix: str) -> Set[str]: """ From 9bc20ed944bdae196c7be4281ba40a2da523cb79 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 7 Nov 2020 10:51:50 -0700 Subject: [PATCH 17/25] Fix required properties in generated models --- .../api/tests/test_inline_objects.py | 9 +--- .../my_test_api_client/models/a_model.py | 45 ++++++++----------- .../models/http_validation_error.py | 23 +++++----- .../my_test_api_client/models/json_body.py | 14 +++--- .../api/tests/test_inline_objects.py | 14 +++--- .../my_test_api_client/models/a_model.py | 45 ++++++++----------- .../models/http_validation_error.py | 23 +++++----- .../my_test_api_client/models/json_body.py | 14 +++--- .../parser/properties/__init__.py | 4 +- 9 files changed, 80 insertions(+), 111 deletions(-) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py index bd9bb9fff..ff473083c 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py @@ -2,10 +2,7 @@ Client = httpx.Client -from typing import Dict, Union - from ...models.json_body import JsonBody -from ...types import UNSET, Unset def _build_response(*, response: httpx.Response) -> httpx.Response[None]: @@ -20,12 +17,10 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[None]: def httpx_request( *, client: Client, - json_body: Union[JsonBody, Unset], + json_body: JsonBody, ) -> httpx.Response[None]: - json_json_body: Dict[str, Any] = UNSET - if not isinstance(json_body, Unset): - json_json_body = json_body.to_dict() + json_json_body = json_body.to_dict() response = client.request( "post", diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py index 87175124c..9599f7fd5 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py @@ -6,7 +6,6 @@ from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum -from ..types import UNSET, Unset @attr.s(auto_attribs=True) @@ -17,11 +16,11 @@ class AModel: a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date required_not_nullable: str + nested_list_of_enums: List[List[DifferentEnum]] + attr_1_leading_digit: str required_nullable: Optional[str] - nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET - attr_1_leading_digit: Union[Unset, str] = UNSET - not_required_nullable: Union[Unset, Optional[str]] = UNSET - not_required_not_nullable: Union[Unset, str] = UNSET + not_required_nullable: Optional[str] + not_required_not_nullable: str def to_dict(self) -> Dict[str, Any]: an_enum_value = self.an_enum_value.value @@ -35,17 +34,15 @@ def to_dict(self) -> Dict[str, Any]: a_date = self.a_date.isoformat() required_not_nullable = self.required_not_nullable - nested_list_of_enums: Union[Unset, List[Any]] = UNSET - if not isinstance(self.nested_list_of_enums, Unset): - nested_list_of_enums = [] - for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value + nested_list_of_enums = [] + for nested_list_of_enums_item_data in self.nested_list_of_enums: + nested_list_of_enums_item = [] + for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: + nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value - nested_list_of_enums_item.append(nested_list_of_enums_item_item) + nested_list_of_enums_item.append(nested_list_of_enums_item_item) - nested_list_of_enums.append(nested_list_of_enums_item) + nested_list_of_enums.append(nested_list_of_enums_item) attr_1_leading_digit = self.attr_1_leading_digit required_nullable = self.required_nullable @@ -57,16 +54,12 @@ def to_dict(self) -> Dict[str, Any]: "aCamelDateTime": a_camel_date_time, "a_date": a_date, "required_not_nullable": required_not_nullable, + "nested_list_of_enums": nested_list_of_enums, + "1_leading_digit": attr_1_leading_digit, "required_nullable": required_nullable, + "not_required_nullable": not_required_nullable, + "not_required_not_nullable": not_required_not_nullable, } - if nested_list_of_enums is not UNSET: - field_dict["nested_list_of_enums"] = nested_list_of_enums - if attr_1_leading_digit is not UNSET: - field_dict["1_leading_digit"] = attr_1_leading_digit - if not_required_nullable is not UNSET: - field_dict["not_required_nullable"] = not_required_nullable - if not_required_not_nullable is not UNSET: - field_dict["not_required_not_nullable"] = not_required_not_nullable return field_dict @@ -93,7 +86,7 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d required_not_nullable = d["required_not_nullable"] nested_list_of_enums = [] - for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: + for nested_list_of_enums_item_data in d["nested_list_of_enums"]: nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) @@ -102,13 +95,13 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d nested_list_of_enums.append(nested_list_of_enums_item) - attr_1_leading_digit = d.get("1_leading_digit", UNSET) + attr_1_leading_digit = d["1_leading_digit"] required_nullable = d["required_nullable"] - not_required_nullable = d.get("not_required_nullable", UNSET) + not_required_nullable = d["not_required_nullable"] - not_required_not_nullable = d.get("not_required_not_nullable", UNSET) + not_required_not_nullable = d["not_required_not_nullable"] return AModel( an_enum_value=an_enum_value, diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py index 9d29faa4d..2b83121ce 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py @@ -1,36 +1,33 @@ -from typing import Any, Dict, List, Union +from typing import Any, Dict, List import attr from ..models.validation_error import ValidationError -from ..types import UNSET, Unset @attr.s(auto_attribs=True) class HTTPValidationError: """ """ - detail: Union[Unset, List[ValidationError]] = UNSET + detail: List[ValidationError] def to_dict(self) -> Dict[str, Any]: - detail: Union[Unset, List[Any]] = UNSET - if not isinstance(self.detail, Unset): - detail = [] - for detail_item_data in self.detail: - detail_item = detail_item_data.to_dict() + detail = [] + for detail_item_data in self.detail: + detail_item = detail_item_data.to_dict() - detail.append(detail_item) + detail.append(detail_item) - field_dict = {} - if detail is not UNSET: - field_dict["detail"] = detail + field_dict = { + "detail": detail, + } return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": detail = [] - for detail_item_data in d.get("detail", UNSET) or []: + for detail_item_data in d["detail"]: detail_item = ValidationError.from_dict(detail_item_data) detail.append(detail_item) diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/json_body.py b/end_to_end_tests/golden-record-custom/my_test_api_client/models/json_body.py index 6d57d2d3f..57577c983 100644 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/models/json_body.py +++ b/end_to_end_tests/golden-record-custom/my_test_api_client/models/json_body.py @@ -1,28 +1,26 @@ -from typing import Any, Dict, Union +from typing import Any, Dict import attr -from ..types import UNSET, Unset - @attr.s(auto_attribs=True) class JsonBody: """ """ - a_property: Union[Unset, str] = UNSET + a_property: str def to_dict(self) -> Dict[str, Any]: a_property = self.a_property - field_dict = {} - if a_property is not UNSET: - field_dict["a_property"] = a_property + field_dict = { + "a_property": a_property, + } return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "JsonBody": - a_property = d.get("a_property", UNSET) + a_property = d["a_property"] return JsonBody( a_property=a_property, diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py index 8374c90fa..d4586cccd 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py @@ -1,24 +1,22 @@ -from typing import Any, Dict, Union +from typing import Any, Dict import httpx from ...client import Client from ...models.json_body import JsonBody -from ...types import UNSET, Response, Unset +from ...types import Response def _get_kwargs( *, client: Client, - json_body: Union[JsonBody, Unset], + json_body: JsonBody, ) -> Dict[str, Any]: url = "{}/tests/inline_objects".format(client.base_url) headers: Dict[str, Any] = client.get_headers() - json_json_body: Dict[str, Any] = UNSET - if not isinstance(json_body, Unset): - json_json_body = json_body.to_dict() + json_json_body = json_body.to_dict() return { "url": url, @@ -41,7 +39,7 @@ def _build_response(*, response: httpx.Response) -> Response[None]: def sync_detailed( *, client: Client, - json_body: Union[JsonBody, Unset], + json_body: JsonBody, ) -> Response[None]: kwargs = _get_kwargs( client=client, @@ -58,7 +56,7 @@ def sync_detailed( async def asyncio_detailed( *, client: Client, - json_body: Union[JsonBody, Unset], + json_body: JsonBody, ) -> Response[None]: kwargs = _get_kwargs( client=client, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index 87175124c..9599f7fd5 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py @@ -6,7 +6,6 @@ from ..models.an_enum import AnEnum from ..models.different_enum import DifferentEnum -from ..types import UNSET, Unset @attr.s(auto_attribs=True) @@ -17,11 +16,11 @@ class AModel: a_camel_date_time: Union[datetime.datetime, datetime.date] a_date: datetime.date required_not_nullable: str + nested_list_of_enums: List[List[DifferentEnum]] + attr_1_leading_digit: str required_nullable: Optional[str] - nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET - attr_1_leading_digit: Union[Unset, str] = UNSET - not_required_nullable: Union[Unset, Optional[str]] = UNSET - not_required_not_nullable: Union[Unset, str] = UNSET + not_required_nullable: Optional[str] + not_required_not_nullable: str def to_dict(self) -> Dict[str, Any]: an_enum_value = self.an_enum_value.value @@ -35,17 +34,15 @@ def to_dict(self) -> Dict[str, Any]: a_date = self.a_date.isoformat() required_not_nullable = self.required_not_nullable - nested_list_of_enums: Union[Unset, List[Any]] = UNSET - if not isinstance(self.nested_list_of_enums, Unset): - nested_list_of_enums = [] - for nested_list_of_enums_item_data in self.nested_list_of_enums: - nested_list_of_enums_item = [] - for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: - nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value + nested_list_of_enums = [] + for nested_list_of_enums_item_data in self.nested_list_of_enums: + nested_list_of_enums_item = [] + for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: + nested_list_of_enums_item_item = nested_list_of_enums_item_item_data.value - nested_list_of_enums_item.append(nested_list_of_enums_item_item) + nested_list_of_enums_item.append(nested_list_of_enums_item_item) - nested_list_of_enums.append(nested_list_of_enums_item) + nested_list_of_enums.append(nested_list_of_enums_item) attr_1_leading_digit = self.attr_1_leading_digit required_nullable = self.required_nullable @@ -57,16 +54,12 @@ def to_dict(self) -> Dict[str, Any]: "aCamelDateTime": a_camel_date_time, "a_date": a_date, "required_not_nullable": required_not_nullable, + "nested_list_of_enums": nested_list_of_enums, + "1_leading_digit": attr_1_leading_digit, "required_nullable": required_nullable, + "not_required_nullable": not_required_nullable, + "not_required_not_nullable": not_required_not_nullable, } - if nested_list_of_enums is not UNSET: - field_dict["nested_list_of_enums"] = nested_list_of_enums - if attr_1_leading_digit is not UNSET: - field_dict["1_leading_digit"] = attr_1_leading_digit - if not_required_nullable is not UNSET: - field_dict["not_required_nullable"] = not_required_nullable - if not_required_not_nullable is not UNSET: - field_dict["not_required_not_nullable"] = not_required_not_nullable return field_dict @@ -93,7 +86,7 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d required_not_nullable = d["required_not_nullable"] nested_list_of_enums = [] - for nested_list_of_enums_item_data in d.get("nested_list_of_enums", UNSET) or []: + for nested_list_of_enums_item_data in d["nested_list_of_enums"]: nested_list_of_enums_item = [] for nested_list_of_enums_item_item_data in nested_list_of_enums_item_data: nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data) @@ -102,13 +95,13 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d nested_list_of_enums.append(nested_list_of_enums_item) - attr_1_leading_digit = d.get("1_leading_digit", UNSET) + attr_1_leading_digit = d["1_leading_digit"] required_nullable = d["required_nullable"] - not_required_nullable = d.get("not_required_nullable", UNSET) + not_required_nullable = d["not_required_nullable"] - not_required_not_nullable = d.get("not_required_not_nullable", UNSET) + not_required_not_nullable = d["not_required_not_nullable"] return AModel( an_enum_value=an_enum_value, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py index 9d29faa4d..2b83121ce 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/http_validation_error.py @@ -1,36 +1,33 @@ -from typing import Any, Dict, List, Union +from typing import Any, Dict, List import attr from ..models.validation_error import ValidationError -from ..types import UNSET, Unset @attr.s(auto_attribs=True) class HTTPValidationError: """ """ - detail: Union[Unset, List[ValidationError]] = UNSET + detail: List[ValidationError] def to_dict(self) -> Dict[str, Any]: - detail: Union[Unset, List[Any]] = UNSET - if not isinstance(self.detail, Unset): - detail = [] - for detail_item_data in self.detail: - detail_item = detail_item_data.to_dict() + detail = [] + for detail_item_data in self.detail: + detail_item = detail_item_data.to_dict() - detail.append(detail_item) + detail.append(detail_item) - field_dict = {} - if detail is not UNSET: - field_dict["detail"] = detail + field_dict = { + "detail": detail, + } return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": detail = [] - for detail_item_data in d.get("detail", UNSET) or []: + for detail_item_data in d["detail"]: detail_item = ValidationError.from_dict(detail_item_data) detail.append(detail_item) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py b/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py index 6d57d2d3f..57577c983 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py @@ -1,28 +1,26 @@ -from typing import Any, Dict, Union +from typing import Any, Dict import attr -from ..types import UNSET, Unset - @attr.s(auto_attribs=True) class JsonBody: """ """ - a_property: Union[Unset, str] = UNSET + a_property: str def to_dict(self) -> Dict[str, Any]: a_property = self.a_property - field_dict = {} - if a_property is not UNSET: - field_dict["a_property"] = a_property + field_dict = { + "a_property": a_property, + } return field_dict @staticmethod def from_dict(d: Dict[str, Any]) -> "JsonBody": - a_property = d.get("a_property", UNSET) + a_property = d["a_property"] return JsonBody( a_property=a_property, diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 833ad4766..8a504945f 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -232,11 +232,11 @@ def build_model_property( ref = Reference.from_ref(data.title or name) for key, value in (data.properties or {}).items(): - required = key in required_set + prop_required = key in required_set prop, schemas = property_from_data(name=key, required=required, data=value, schemas=schemas) if isinstance(prop, PropertyError): return prop, schemas - if required and not prop.nullable: + if prop_required and not prop.nullable: required_properties.append(prop) else: optional_properties.append(prop) From c8ae42dbdcdebde7da221dbb38ef37a4967837f2 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 7 Nov 2020 10:56:15 -0700 Subject: [PATCH 18/25] Fix custom template E2E test --- .../golden-record-custom/README.md | 18 +++++++++--------- .../__init__.py | 0 .../api/__init__.py | 0 .../api/tests/__init__.py | 0 .../api/tests/defaults_tests_defaults_post.py | 0 .../api/tests/get_basic_list_of_booleans.py | 0 .../api/tests/get_basic_list_of_floats.py | 0 .../api/tests/get_basic_list_of_integers.py | 0 .../api/tests/get_basic_list_of_strings.py | 0 .../api/tests/get_user_list.py | 0 .../api/tests/int_enum_tests_int_enum_post.py | 0 .../tests/json_body_tests_json_body_post.py | 0 .../tests/no_response_tests_no_response_get.py | 0 .../octet_stream_tests_octet_stream_get.py | 0 ...ptional_value_tests_optional_query_param.py | 0 .../api/tests/test_inline_objects.py | 0 ...ed_content_tests_unsupported_content_get.py | 0 .../api/tests/upload_file_tests_upload_post.py | 0 .../client.py | 0 .../models/__init__.py | 0 .../models/a_model.py | 0 .../models/an_enum.py | 0 .../models/an_int_enum.py | 0 .../body_upload_file_tests_upload_post.py | 0 .../models/different_enum.py | 0 .../models/http_validation_error.py | 0 .../models/json_body.py | 0 .../models/validation_error.py | 0 .../py.typed | 0 .../types.py | 0 .../golden-record-custom/pyproject.toml | 6 +++--- end_to_end_tests/regen_golden_record.py | 7 ++++--- 32 files changed, 16 insertions(+), 15 deletions(-) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/__init__.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/__init__.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/__init__.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/defaults_tests_defaults_post.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/get_basic_list_of_booleans.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/get_basic_list_of_floats.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/get_basic_list_of_integers.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/get_basic_list_of_strings.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/get_user_list.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/int_enum_tests_int_enum_post.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/json_body_tests_json_body_post.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/no_response_tests_no_response_get.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/octet_stream_tests_octet_stream_get.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/optional_value_tests_optional_query_param.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/test_inline_objects.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/unsupported_content_tests_unsupported_content_get.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/api/tests/upload_file_tests_upload_post.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/client.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/models/__init__.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/models/a_model.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/models/an_enum.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/models/an_int_enum.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/models/body_upload_file_tests_upload_post.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/models/different_enum.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/models/http_validation_error.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/models/json_body.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/models/validation_error.py (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/py.typed (100%) rename end_to_end_tests/golden-record-custom/{my_test_api_client => custom_e2e}/types.py (100%) diff --git a/end_to_end_tests/golden-record-custom/README.md b/end_to_end_tests/golden-record-custom/README.md index fbbe00c2f..dcc70600f 100644 --- a/end_to_end_tests/golden-record-custom/README.md +++ b/end_to_end_tests/golden-record-custom/README.md @@ -1,11 +1,11 @@ -# my-test-api-client +# custom-e2e A client library for accessing My Test API ## Usage First, create a client: ```python -from my_test_api_client import Client +from custom_e2e import Client client = Client(base_url="https://api.example.com") ``` @@ -13,7 +13,7 @@ client = Client(base_url="https://api.example.com") If the endpoints you're going to hit require authentication, use `AuthenticatedClient` instead: ```python -from my_test_api_client import AuthenticatedClient +from custom_e2e import AuthenticatedClient client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSecretToken") ``` @@ -21,8 +21,8 @@ client = AuthenticatedClient(base_url="https://api.example.com", token="SuperSec Now call your endpoint and use your models: ```python -from my_test_api_client.models import MyDataModel -from my_test_api_client.api.my_tag import get_my_data_model +from custom_e2e.models import MyDataModel +from custom_e2e.api.my_tag import get_my_data_model my_data: MyDataModel = get_my_data_model(client=client) ``` @@ -30,8 +30,8 @@ my_data: MyDataModel = get_my_data_model(client=client) Or do the same thing with an async version: ```python -from my_test_api_client.models import MyDataModel -from my_test_api_client.async_api.my_tag import get_my_data_model +from custom_e2e.models import MyDataModel +from custom_e2e.async_api.my_tag import get_my_data_model my_data: MyDataModel = await get_my_data_model(client=client) ``` @@ -40,9 +40,9 @@ Things to know: 1. Every path/method combo becomes a Python function with type annotations. 1. All path/query params, and bodies become method arguments. 1. If your endpoint had any tags on it, the first tag will be used as a module name for the function (my_tag above) -1. Any endpoint which did not have a tag will be in `my_test_api_client.api.default` +1. Any endpoint which did not have a tag will be in `custom_e2e.api.default` 1. If the API returns a response code that was not declared in the OpenAPI document, a - `my_test_api_client.api.errors.ApiResponseError` wil be raised + `custom_e2e.api.errors.ApiResponseError` wil be raised with the `response` attribute set to the `httpx.Response` that was received. diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/__init__.py b/end_to_end_tests/golden-record-custom/custom_e2e/__init__.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/__init__.py rename to end_to_end_tests/golden-record-custom/custom_e2e/__init__.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/__init__.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/__init__.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/__init__.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/__init__.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/__init__.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/__init__.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/__init__.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/__init__.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/defaults_tests_defaults_post.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_basic_list_of_booleans.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_booleans.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_basic_list_of_booleans.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_booleans.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_basic_list_of_floats.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_floats.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_basic_list_of_floats.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_floats.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_basic_list_of_integers.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_integers.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_basic_list_of_integers.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_integers.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_basic_list_of_strings.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_strings.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_basic_list_of_strings.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_strings.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/get_user_list.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/int_enum_tests_int_enum_post.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/int_enum_tests_int_enum_post.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/json_body_tests_json_body_post.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/json_body_tests_json_body_post.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/json_body_tests_json_body_post.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/no_response_tests_no_response_get.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/no_response_tests_no_response_get.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/no_response_tests_no_response_get.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/no_response_tests_no_response_get.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/octet_stream_tests_octet_stream_get.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/octet_stream_tests_octet_stream_get.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/test_inline_objects.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/unsupported_content_tests_unsupported_content_get.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/unsupported_content_tests_unsupported_content_get.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/unsupported_content_tests_unsupported_content_get.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/api/tests/upload_file_tests_upload_post.py rename to end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/client.py b/end_to_end_tests/golden-record-custom/custom_e2e/client.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/client.py rename to end_to_end_tests/golden-record-custom/custom_e2e/client.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/__init__.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/models/__init__.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/models/a_model.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/an_enum.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/an_enum.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/models/an_enum.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/an_enum.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/an_int_enum.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/an_int_enum.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/models/an_int_enum.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/an_int_enum.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/body_upload_file_tests_upload_post.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/models/body_upload_file_tests_upload_post.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/body_upload_file_tests_upload_post.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/different_enum.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/different_enum.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/models/different_enum.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/different_enum.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/http_validation_error.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/models/http_validation_error.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/http_validation_error.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/json_body.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/json_body.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/models/json_body.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/json_body.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/validation_error.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/models/validation_error.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/validation_error.py diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/py.typed b/end_to_end_tests/golden-record-custom/custom_e2e/py.typed similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/py.typed rename to end_to_end_tests/golden-record-custom/custom_e2e/py.typed diff --git a/end_to_end_tests/golden-record-custom/my_test_api_client/types.py b/end_to_end_tests/golden-record-custom/custom_e2e/types.py similarity index 100% rename from end_to_end_tests/golden-record-custom/my_test_api_client/types.py rename to end_to_end_tests/golden-record-custom/custom_e2e/types.py diff --git a/end_to_end_tests/golden-record-custom/pyproject.toml b/end_to_end_tests/golden-record-custom/pyproject.toml index eeb1a9e4e..bf0748d1d 100644 --- a/end_to_end_tests/golden-record-custom/pyproject.toml +++ b/end_to_end_tests/golden-record-custom/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "my-test-api-client" +name = "custom-e2e" version = "0.1.0" description = "A client library for accessing My Test API" @@ -7,9 +7,9 @@ authors = [] readme = "README.md" packages = [ - {include = "my_test_api_client"}, + {include = "custom_e2e"}, ] -include = ["CHANGELOG.md", "my_test_api_client/py.typed"] +include = ["CHANGELOG.md", "custom_e2e/py.typed"] [tool.poetry.dependencies] diff --git a/end_to_end_tests/regen_golden_record.py b/end_to_end_tests/regen_golden_record.py index 68377138e..1532946d8 100644 --- a/end_to_end_tests/regen_golden_record.py +++ b/end_to_end_tests/regen_golden_record.py @@ -11,17 +11,18 @@ runner = CliRunner() openapi_path = Path(__file__).parent / "openapi.json" - output_path = Path.cwd() / "my-test-api-client" - custom = len(sys.argv) >= 2 and sys.argv[1] == "custom" if custom: gr_path = Path(__file__).parent / "golden-record-custom" + output_path = Path.cwd() / "custom-e2e" + config_path = Path(__file__).parent / "custom_config.yml" else: gr_path = Path(__file__).parent / "golden-record" + output_path = Path.cwd() / "my-test-api-client" + config_path = Path(__file__).parent / "config.yml" shutil.rmtree(gr_path, ignore_errors=True) shutil.rmtree(output_path, ignore_errors=True) - config_path = Path(__file__).parent / "config.yml" if custom: result = runner.invoke( From 4743ae8e48039aec1fba65916afb97671365e478 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 7 Nov 2020 14:28:36 -0700 Subject: [PATCH 19/25] Refactored response handling to use the same schema generation as input properties. --- CHANGELOG.md | 6 + .../api/tests/defaults_tests_defaults_post.py | 10 +- .../api/tests/get_basic_list_of_booleans.py | 6 +- .../api/tests/get_basic_list_of_floats.py | 6 +- .../api/tests/get_basic_list_of_integers.py | 6 +- .../api/tests/get_basic_list_of_strings.py | 6 +- .../custom_e2e/api/tests/get_user_list.py | 14 +- .../api/tests/int_enum_tests_int_enum_post.py | 10 +- .../tests/json_body_tests_json_body_post.py | 10 +- .../octet_stream_tests_octet_stream_get.py | 14 +- ...tional_value_tests_optional_query_param.py | 10 +- .../api/tests/test_inline_objects.py | 17 +- .../tests/upload_file_tests_upload_post.py | 10 +- .../custom_e2e/models/__init__.py | 1 + .../body_upload_file_tests_upload_post.py | 3 +- .../custom_e2e/models/response_200.py | 27 ++ .../custom_e2e/models/validation_error.py | 4 +- .../api/tests/defaults_tests_defaults_post.py | 10 +- .../api/tests/get_basic_list_of_booleans.py | 4 +- .../api/tests/get_basic_list_of_floats.py | 4 +- .../api/tests/get_basic_list_of_integers.py | 4 +- .../api/tests/get_basic_list_of_strings.py | 4 +- .../api/tests/get_user_list.py | 14 +- .../api/tests/int_enum_tests_int_enum_post.py | 10 +- .../tests/json_body_tests_json_body_post.py | 10 +- .../octet_stream_tests_octet_stream_get.py | 19 +- ...tional_value_tests_optional_query_param.py | 10 +- .../api/tests/test_inline_objects.py | 47 ++- .../tests/upload_file_tests_upload_post.py | 10 +- .../my_test_api_client/models/__init__.py | 1 + .../body_upload_file_tests_upload_post.py | 3 +- .../my_test_api_client/models/response_200.py | 27 ++ .../models/validation_error.py | 4 +- .../test_custom_templates/endpoint_module.pyi | 8 +- openapi_python_client/parser/openapi.py | 13 +- .../parser/properties/__init__.py | 21 +- openapi_python_client/parser/responses.py | 176 +++-------- .../templates/endpoint_macros.pyi | 4 +- .../templates/endpoint_module.pyi | 8 +- .../property_templates/file_property.pyi | 5 +- .../property_templates/list_property.pyi | 2 +- .../property_templates/none_property.pyi | 7 + tests/test_parser/test_openapi.py | 92 ++++-- .../test_parser/test_properties/test_init.py | 117 ++------ tests/test_parser/test_responses.py | 283 +----------------- tests/test_templates/endpoint_module.py | 22 +- tests/test_templates/test_endpoint_module.py | 15 +- 47 files changed, 467 insertions(+), 647 deletions(-) create mode 100644 end_to_end_tests/golden-record-custom/custom_e2e/models/response_200.py create mode 100644 end_to_end_tests/golden-record/my_test_api_client/models/response_200.py create mode 100644 openapi_python_client/templates/property_templates/none_property.pyi diff --git a/CHANGELOG.md b/CHANGELOG.md index 867fe751c..5cabe053c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Any request/response field that is not `required` and wasn't specified is now set to `UNSET` instead of `None`. - Values that are `UNSET` will not be sent along in API calls +- Schemas defined with `type=object` will now be converted into classes, just like if they were created as ref components. + The previous behavior was a combination of skipping and using generic Dicts for these schemas. +- Response schema handling was unified with input schema handling, meaning that responses will behave differently than before. + Specifically, instead of the content-type deciding what the generated Python type is, the schema itself will. +- Instead of skipping input properties with no type, enum, anyOf, or oneOf declared, the property will be declared as `None`. ### Additions - Added a `--custom-template-path` option for providing custom jinja2 templates (#231 - Thanks @erichulburd!). - Better compatibility for "required" (whether or not the field must be included) and "nullable" (whether or not the field can be null) (#205 & #208). Thanks @bowenwr & @emannguitar! +- Support for all the same schemas in responses as are supported in parameters. ## 0.6.2 - 2020-11-03 diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py index fb6cf0f1f..c40d9ae7f 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py @@ -5,7 +5,7 @@ Client = httpx.Client import datetime -from typing import List, Union, cast +from typing import Dict, List, Union from dateutil.parser import isoparse @@ -16,9 +16,13 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: if response.status_code == 200: - return None + response_200 = None + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_booleans.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_booleans.py index 20d7c43ab..7011197a0 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_booleans.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_booleans.py @@ -4,10 +4,14 @@ Client = httpx.Client +from typing import List, cast + def _parse_response(*, response: httpx.Response) -> Optional[List[bool]]: if response.status_code == 200: - return [bool(item) for item in cast(List[bool], response.json())] + response_200 = cast(List[bool], response.json()) + + return response_200 return None diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_floats.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_floats.py index e3fc2e4d4..0af3dee86 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_floats.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_floats.py @@ -4,10 +4,14 @@ Client = httpx.Client +from typing import List, cast + def _parse_response(*, response: httpx.Response) -> Optional[List[float]]: if response.status_code == 200: - return [float(item) for item in cast(List[float], response.json())] + response_200 = cast(List[float], response.json()) + + return response_200 return None diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_integers.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_integers.py index 28ec4963c..76a227cd5 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_integers.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_integers.py @@ -4,10 +4,14 @@ Client = httpx.Client +from typing import List, cast + def _parse_response(*, response: httpx.Response) -> Optional[List[int]]: if response.status_code == 200: - return [int(item) for item in cast(List[int], response.json())] + response_200 = cast(List[int], response.json()) + + return response_200 return None diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_strings.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_strings.py index 1acdf6a40..7205112f4 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_strings.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_strings.py @@ -4,10 +4,14 @@ Client = httpx.Client +from typing import List, cast + def _parse_response(*, response: httpx.Response) -> Optional[List[str]]: if response.status_code == 200: - return [str(item) for item in cast(List[str], response.json())] + response_200 = cast(List[str], response.json()) + + return response_200 return None diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py index 951ea518a..204467253 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py @@ -5,7 +5,7 @@ Client = httpx.Client import datetime -from typing import List, Union, cast +from typing import Dict, List, Union from ...models.a_model import AModel from ...models.an_enum import AnEnum @@ -14,9 +14,17 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[List[AModel], HTTPValidationError]]: if response.status_code == 200: - return [AModel.from_dict(item) for item in cast(List[Dict[str, Any]], response.json())] + response_200 = [] + for response_200_item_data in response.json(): + response_200_item = AModel.from_dict(response_200_item_data) + + response_200.append(response_200_item) + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/int_enum_tests_int_enum_post.py index 73f4c7605..d10cb6e9b 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/int_enum_tests_int_enum_post.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/int_enum_tests_int_enum_post.py @@ -4,15 +4,21 @@ Client = httpx.Client +from typing import Dict + from ...models.an_int_enum import AnIntEnum from ...models.http_validation_error import HTTPValidationError def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: if response.status_code == 200: - return None + response_200 = None + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/json_body_tests_json_body_post.py index 76f4ffe84..ad8bb7c67 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/json_body_tests_json_body_post.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/json_body_tests_json_body_post.py @@ -4,17 +4,19 @@ Client = httpx.Client -from typing import Dict, cast - from ...models.a_model import AModel from ...models.http_validation_error import HTTPValidationError def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: if response.status_code == 200: - return None + response_200 = None + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/octet_stream_tests_octet_stream_get.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/octet_stream_tests_octet_stream_get.py index 8f1b83adb..1fd6e3637 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/octet_stream_tests_octet_stream_get.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/octet_stream_tests_octet_stream_get.py @@ -4,14 +4,20 @@ Client = httpx.Client +from io import BytesIO -def _parse_response(*, response: httpx.Response) -> Optional[bytes]: +from ...types import File + + +def _parse_response(*, response: httpx.Response) -> Optional[File]: if response.status_code == 200: - return bytes(response.content) + response_200 = File(payload=BytesIO(response.content)) + + return response_200 return None -def _build_response(*, response: httpx.Response) -> httpx.Response[bytes]: +def _build_response(*, response: httpx.Response) -> httpx.Response[File]: return httpx.Response( status_code=response.status_code, content=response.content, @@ -23,7 +29,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[bytes]: def httpx_request( *, client: Client, -) -> httpx.Response[bytes]: +) -> httpx.Response[File]: response = client.request( "get", diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py index 274a28aff..68c5e2d98 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py @@ -4,7 +4,7 @@ Client = httpx.Client -from typing import List, Union +from typing import Dict, List, Union from ...models.http_validation_error import HTTPValidationError from ...types import UNSET, Unset @@ -12,9 +12,13 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: if response.status_code == 200: - return None + response_200 = None + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py index ff473083c..4d4d867f9 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py @@ -1,16 +1,27 @@ +from typing import Optional + import httpx Client = httpx.Client from ...models.json_body import JsonBody +from ...models.response_200 import Response_200 + + +def _parse_response(*, response: httpx.Response) -> Optional[Response_200]: + if response.status_code == 200: + response_200 = Response_200.from_dict(response.json()) + + return response_200 + return None -def _build_response(*, response: httpx.Response) -> httpx.Response[None]: +def _build_response(*, response: httpx.Response) -> httpx.Response[Response_200]: return httpx.Response( status_code=response.status_code, content=response.content, headers=response.headers, - parsed=None, + parsed=_parse_response(response=response), ) @@ -18,7 +29,7 @@ def httpx_request( *, client: Client, json_body: JsonBody, -) -> httpx.Response[None]: +) -> httpx.Response[Response_200]: json_json_body = json_body.to_dict() diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py index e658e2f04..91153fbfd 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py @@ -4,7 +4,7 @@ Client = httpx.Client -from typing import Union +from typing import Dict, Union, cast from ...models.body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from ...models.http_validation_error import HTTPValidationError @@ -16,9 +16,13 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[ HTTPValidationError ]]: if response.status_code == 200: - return None + response_200 = None + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py index 210b8b01d..2f0aa1f1a 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py @@ -7,4 +7,5 @@ from .different_enum import DifferentEnum from .http_validation_error import HTTPValidationError from .json_body import JsonBody +from .response_200 import Response_200 from .validation_error import ValidationError diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/body_upload_file_tests_upload_post.py index 3435bd290..93f2dd68f 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/body_upload_file_tests_upload_post.py @@ -1,3 +1,4 @@ +from io import BytesIO from typing import Any, Dict import attr @@ -22,7 +23,7 @@ def to_dict(self) -> Dict[str, Any]: @staticmethod def from_dict(d: Dict[str, Any]) -> "BodyUploadFileTestsUploadPost": - some_file = d["some_file"] + some_file = File(payload=BytesIO(d["some_file"])) return BodyUploadFileTestsUploadPost( some_file=some_file, diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/response_200.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/response_200.py new file mode 100644 index 000000000..0055f6699 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/response_200.py @@ -0,0 +1,27 @@ +from typing import Any, Dict + +import attr + + +@attr.s(auto_attribs=True) +class Response_200: + """ """ + + a_property: str + + def to_dict(self) -> Dict[str, Any]: + a_property = self.a_property + + field_dict = { + "a_property": a_property, + } + + return field_dict + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "Response_200": + a_property = d["a_property"] + + return Response_200( + a_property=a_property, + ) diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/validation_error.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/validation_error.py index 77b9239ef..cadbc0d5b 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/validation_error.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/validation_error.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List +from typing import Any, Dict, List, cast import attr @@ -27,7 +27,7 @@ def to_dict(self) -> Dict[str, Any]: @staticmethod def from_dict(d: Dict[str, Any]) -> "ValidationError": - loc = d["loc"] + loc = cast(List[str], d["loc"]) msg = d["msg"] diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py index 77da9d588..b289fafed 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/defaults_tests_defaults_post.py @@ -1,5 +1,5 @@ import datetime -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Dict, List, Optional, Union import httpx from dateutil.parser import isoparse @@ -86,9 +86,13 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: if response.status_code == 200: - return None + response_200 = None + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_booleans.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_booleans.py index fdded907f..eeedd5337 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_booleans.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_booleans.py @@ -24,7 +24,9 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[List[bool]]: if response.status_code == 200: - return [bool(item) for item in cast(List[bool], response.json())] + response_200 = cast(List[bool], response.json()) + + return response_200 return None diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_floats.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_floats.py index 3c13bba68..84735b823 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_floats.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_floats.py @@ -24,7 +24,9 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[List[float]]: if response.status_code == 200: - return [float(item) for item in cast(List[float], response.json())] + response_200 = cast(List[float], response.json()) + + return response_200 return None diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_integers.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_integers.py index c90284532..56197de7c 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_integers.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_integers.py @@ -24,7 +24,9 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[List[int]]: if response.status_code == 200: - return [int(item) for item in cast(List[int], response.json())] + response_200 = cast(List[int], response.json()) + + return response_200 return None diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_strings.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_strings.py index 770625240..d75f452fb 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_strings.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_basic_list_of_strings.py @@ -24,7 +24,9 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[List[str]]: if response.status_code == 200: - return [str(item) for item in cast(List[str], response.json())] + response_200 = cast(List[str], response.json()) + + return response_200 return None diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py index 815e0b02e..04f46d685 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/get_user_list.py @@ -1,5 +1,5 @@ import datetime -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Dict, List, Optional, Union import httpx @@ -48,9 +48,17 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[Union[List[AModel], HTTPValidationError]]: if response.status_code == 200: - return [AModel.from_dict(item) for item in cast(List[Dict[str, Any]], response.json())] + response_200 = [] + for response_200_item_data in response.json(): + response_200_item = AModel.from_dict(response_200_item_data) + + response_200.append(response_200_item) + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py index e15ce2e2c..cace678f1 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/int_enum_tests_int_enum_post.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Union, cast +from typing import Any, Dict, Optional, Union import httpx @@ -34,9 +34,13 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: if response.status_code == 200: - return None + response_200 = None + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py index eb556c5d7..408f2dab1 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/json_body_tests_json_body_post.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Union, cast +from typing import Any, Dict, Optional, Union import httpx @@ -30,9 +30,13 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: if response.status_code == 200: - return None + response_200 = None + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py index 7d8ee3846..753b64a13 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/octet_stream_tests_octet_stream_get.py @@ -1,9 +1,10 @@ +from io import BytesIO from typing import Any, Dict, Optional import httpx from ...client import Client -from ...types import Response +from ...types import File, Response def _get_kwargs( @@ -22,13 +23,15 @@ def _get_kwargs( } -def _parse_response(*, response: httpx.Response) -> Optional[bytes]: +def _parse_response(*, response: httpx.Response) -> Optional[File]: if response.status_code == 200: - return bytes(response.content) + response_200 = File(payload=BytesIO(response.content)) + + return response_200 return None -def _build_response(*, response: httpx.Response) -> Response[bytes]: +def _build_response(*, response: httpx.Response) -> Response[File]: return Response( status_code=response.status_code, content=response.content, @@ -40,7 +43,7 @@ def _build_response(*, response: httpx.Response) -> Response[bytes]: def sync_detailed( *, client: Client, -) -> Response[bytes]: +) -> Response[File]: kwargs = _get_kwargs( client=client, ) @@ -55,7 +58,7 @@ def sync_detailed( def sync( *, client: Client, -) -> Optional[bytes]: +) -> Optional[File]: """ """ return sync_detailed( @@ -66,7 +69,7 @@ def sync( async def asyncio_detailed( *, client: Client, -) -> Response[bytes]: +) -> Response[File]: kwargs = _get_kwargs( client=client, ) @@ -80,7 +83,7 @@ async def asyncio_detailed( async def asyncio( *, client: Client, -) -> Optional[bytes]: +) -> Optional[File]: """ """ return ( diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py index 519c543ac..751f48e03 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/optional_value_tests_optional_query_param.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Union, cast +from typing import Any, Dict, List, Optional, Union import httpx @@ -35,9 +35,13 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: if response.status_code == 200: - return None + response_200 = None + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py index d4586cccd..07161897e 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py @@ -1,9 +1,10 @@ -from typing import Any, Dict +from typing import Any, Dict, Optional import httpx from ...client import Client from ...models.json_body import JsonBody +from ...models.response_200 import Response_200 from ...types import Response @@ -27,12 +28,20 @@ def _get_kwargs( } -def _build_response(*, response: httpx.Response) -> Response[None]: +def _parse_response(*, response: httpx.Response) -> Optional[Response_200]: + if response.status_code == 200: + response_200 = Response_200.from_dict(response.json()) + + return response_200 + return None + + +def _build_response(*, response: httpx.Response) -> Response[Response_200]: return Response( status_code=response.status_code, content=response.content, headers=response.headers, - parsed=None, + parsed=_parse_response(response=response), ) @@ -40,7 +49,7 @@ def sync_detailed( *, client: Client, json_body: JsonBody, -) -> Response[None]: +) -> Response[Response_200]: kwargs = _get_kwargs( client=client, json_body=json_body, @@ -53,11 +62,24 @@ def sync_detailed( return _build_response(response=response) +def sync( + *, + client: Client, + json_body: JsonBody, +) -> Optional[Response_200]: + """ """ + + return sync_detailed( + client=client, + json_body=json_body, + ).parsed + + async def asyncio_detailed( *, client: Client, json_body: JsonBody, -) -> Response[None]: +) -> Response[Response_200]: kwargs = _get_kwargs( client=client, json_body=json_body, @@ -67,3 +89,18 @@ async def asyncio_detailed( response = await _client.post(**kwargs) return _build_response(response=response) + + +async def asyncio( + *, + client: Client, + json_body: JsonBody, +) -> Optional[Response_200]: + """ """ + + return ( + await asyncio_detailed( + client=client, + json_body=json_body, + ) + ).parsed diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py index d0b31d9f7..f8a54ec77 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/upload_file_tests_upload_post.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Union, cast +from typing import Any, Dict, Optional, Union import httpx @@ -32,9 +32,13 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPValidationError]]: if response.status_code == 200: - return None + response_200 = None + + return response_200 if response.status_code == 422: - return HTTPValidationError.from_dict(cast(Dict[str, Any], response.json())) + response_422 = HTTPValidationError.from_dict(response.json()) + + return response_422 return None 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 210b8b01d..2f0aa1f1a 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 @@ -7,4 +7,5 @@ from .different_enum import DifferentEnum from .http_validation_error import HTTPValidationError from .json_body import JsonBody +from .response_200 import Response_200 from .validation_error import ValidationError diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py index 3435bd290..93f2dd68f 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py @@ -1,3 +1,4 @@ +from io import BytesIO from typing import Any, Dict import attr @@ -22,7 +23,7 @@ def to_dict(self) -> Dict[str, Any]: @staticmethod def from_dict(d: Dict[str, Any]) -> "BodyUploadFileTestsUploadPost": - some_file = d["some_file"] + some_file = File(payload=BytesIO(d["some_file"])) return BodyUploadFileTestsUploadPost( some_file=some_file, diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/response_200.py b/end_to_end_tests/golden-record/my_test_api_client/models/response_200.py new file mode 100644 index 000000000..0055f6699 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/response_200.py @@ -0,0 +1,27 @@ +from typing import Any, Dict + +import attr + + +@attr.s(auto_attribs=True) +class Response_200: + """ """ + + a_property: str + + def to_dict(self) -> Dict[str, Any]: + a_property = self.a_property + + field_dict = { + "a_property": a_property, + } + + return field_dict + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "Response_200": + a_property = d["a_property"] + + return Response_200( + a_property=a_property, + ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py index 77b9239ef..cadbc0d5b 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/validation_error.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List +from typing import Any, Dict, List, cast import attr @@ -27,7 +27,7 @@ def to_dict(self) -> Dict[str, Any]: @staticmethod def from_dict(d: Dict[str, Any]) -> "ValidationError": - loc = d["loc"] + loc = cast(List[str], d["loc"]) msg = d["msg"] diff --git a/end_to_end_tests/test_custom_templates/endpoint_module.pyi b/end_to_end_tests/test_custom_templates/endpoint_module.pyi index 57813e1c1..34b46c329 100644 --- a/end_to_end_tests/test_custom_templates/endpoint_module.pyi +++ b/end_to_end_tests/test_custom_templates/endpoint_module.pyi @@ -18,7 +18,13 @@ Client = httpx.Client def _parse_response(*, response: httpx.Response) -> Optional[{{ return_string }}]: {% for response in endpoint.responses %} if response.status_code == {{ response.status_code }}: - return {{ response.constructor() }} + {% if response.prop.template %} + {% from "property_templates/" + response.prop.template import construct %} + {{ construct(response.prop, response.source) | indent(8) }} + {% else %} + {{ response.prop.python_name }} = {{ response.source }} + {% endif %} + return {{ response.prop.python_name }} {% endfor %} return None {% endif %} diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 5c300097e..0c42790d9 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -10,7 +10,7 @@ from .errors import GeneratorError, ParseError, PropertyError from .properties import EnumProperty, ModelProperty, Property, Schemas, build_schemas, property_from_data from .reference import Reference -from .responses import ListRefResponse, RefResponse, Response, response_from_data +from .responses import Response, response_from_data class ParameterLocation(str, Enum): @@ -158,10 +158,10 @@ def _add_body( return endpoint, schemas @staticmethod - def _add_responses(endpoint: "Endpoint", data: oai.Responses) -> "Endpoint": + def _add_responses(*, endpoint: "Endpoint", data: oai.Responses, schemas: Schemas) -> Tuple["Endpoint", Schemas]: endpoint = deepcopy(endpoint) for code, response_data in data.items(): - response = response_from_data(status_code=int(code), data=response_data) + response, schemas = response_from_data(status_code=int(code), data=response_data, schemas=schemas) if isinstance(response, ParseError): endpoint.errors.append( ParseError( @@ -173,10 +173,9 @@ def _add_responses(endpoint: "Endpoint", data: oai.Responses) -> "Endpoint": ) ) continue - if isinstance(response, (RefResponse, ListRefResponse)): - endpoint.relative_imports.add(import_string_from_reference(response.reference, prefix="...models")) + endpoint.relative_imports |= response.prop.get_imports(prefix="...") endpoint.responses.append(response) - return endpoint + return endpoint, schemas @staticmethod def _add_parameters( @@ -228,7 +227,7 @@ def from_data( result, schemas = Endpoint._add_parameters(endpoint=endpoint, data=data, schemas=schemas) if isinstance(result, ParseError): return result, schemas - result = Endpoint._add_responses(result, data.responses) + result, schemas = Endpoint._add_responses(endpoint=result, data=data.responses, schemas=schemas) result, schemas = Endpoint._add_body(endpoint=result, data=data, schemas=schemas) return result, schemas diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 8a504945f..ec63875ef 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -1,5 +1,5 @@ from itertools import chain -from typing import Any, ClassVar, Dict, Generic, List, Optional, Set, Tuple, TypeVar, Union +from typing import Any, ClassVar, Dict, Generic, Iterable, List, Optional, Set, Tuple, TypeVar, Union import attr @@ -14,6 +14,14 @@ from .schemas import Schemas +@attr.s(auto_attribs=True, frozen=True, slots=True) +class NoneProperty(Property): + """ A property that is always None (used for empty schemas) """ + + _type_string: ClassVar[str] = "None" + template: ClassVar[Optional[str]] = "none_property.pyi" + + @attr.s(auto_attribs=True, frozen=True, slots=True) class StringProperty(Property): """ A property of type str """ @@ -81,7 +89,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: back to the root of the generated client. """ imports = super().get_imports(prefix=prefix) - imports.update({f"from {prefix}types import File"}) + imports.update({f"from {prefix}types import File", "from io import BytesIO"}) return imports @@ -137,7 +145,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: """ imports = super().get_imports(prefix=prefix) imports.update(self.inner_property.get_imports(prefix=prefix)) - imports.add("from typing import List") + imports.add("from typing import cast, List") return imports @@ -369,7 +377,7 @@ def _property_from_data( if data.anyOf or data.oneOf: return build_union_property(data=data, name=name, required=required, schemas=schemas) if not data.type: - return PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined."), schemas + return NoneProperty(name=name, required=required, nullable=False, default=None), schemas if data.type == "string": return _string_based_property(name=name, required=required, data=data), schemas @@ -423,6 +431,7 @@ def property_from_data( def update_schemas_with_data(name: str, data: oai.Schema, schemas: Schemas) -> Union[Schemas, PropertyError]: + prop: Union[PropertyError, ModelProperty, EnumProperty] if data.enum is not None: prop, schemas = build_enum_property(data=data, name=name, required=True, schemas=schemas, enum=data.enum) else: @@ -436,9 +445,9 @@ def update_schemas_with_data(name: str, data: oai.Schema, schemas: Schemas) -> U def build_schemas(*, components: Dict[str, Union[oai.Reference, oai.Schema]]) -> Schemas: """ Get a list of Schemas from an OpenAPI dict """ schemas = Schemas() - to_process = components.items() + to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Schema]]] = components.items() processing = True - errors = [] + errors: List[PropertyError] = [] # References could have forward References so keep going as long as we are making progress while processing: diff --git a/openapi_python_client/parser/responses.py b/openapi_python_client/parser/responses.py index 871714722..32b3a4fe2 100644 --- a/openapi_python_client/parser/responses.py +++ b/openapi_python_client/parser/responses.py @@ -1,154 +1,66 @@ -from dataclasses import InitVar, dataclass, field -from typing import Union +from typing import Tuple, Union + +import attr from .. import schema as oai -from .errors import ParseError -from .reference import Reference +from .errors import ParseError, PropertyError +from .properties import NoneProperty, Property, Schemas, property_from_data -@dataclass +@attr.s(auto_attribs=True, frozen=True, slots=True) class Response: """ Describes a single response for an endpoint """ status_code: int + prop: Property + source: str - def return_string(self) -> str: - """ How this Response should be represented as a return type """ - return "None" - - def constructor(self) -> str: - """ How the return value of this response should be constructed """ - return "None" - - -@dataclass -class ListRefResponse(Response): - """ Response is a list of some ref schema """ - - reference: Reference - - def return_string(self) -> str: - """ How this Response should be represented as a return type """ - return f"List[{self.reference.class_name}]" - - def constructor(self) -> str: - """ How the return value of this response should be constructed """ - return f"[{self.reference.class_name}.from_dict(item) for item in cast(List[Dict[str, Any]], response.json())]" - - -@dataclass -class RefResponse(Response): - """ Response is a single ref schema """ - - reference: Reference - - def return_string(self) -> str: - """ How this Response should be represented as a return type """ - return self.reference.class_name - - def constructor(self) -> str: - """ How the return value of this response should be constructed """ - return f"{self.reference.class_name}.from_dict(cast(Dict[str, Any], response.json()))" - - -@dataclass -class ListBasicResponse(Response): - """ Response is a list of some basic type """ - - openapi_type: InitVar[str] - python_type: str = field(init=False) - - def __post_init__(self, openapi_type: str) -> None: - self.python_type = openapi_types_to_python_type_strings[openapi_type] - - def return_string(self) -> str: - """ How this Response should be represented as a return type """ - return f"List[{self.python_type}]" - - def constructor(self) -> str: - """ How the return value of this response should be constructed """ - return f"[{self.python_type}(item) for item in cast(List[{self.python_type}], response.json())]" - -@dataclass -class BasicResponse(Response): - """ Response is a basic type """ - - openapi_type: InitVar[str] - python_type: str = field(init=False) - - def __post_init__(self, openapi_type: str) -> None: - self.python_type = openapi_types_to_python_type_strings[openapi_type] - - def return_string(self) -> str: - """ How this Response should be represented as a return type """ - return self.python_type - - def constructor(self) -> str: - """ How the return value of this response should be constructed """ - return f"{self.python_type}(response.text)" - - -@dataclass -class BytesResponse(Response): - """ Response is a basic type """ - - python_type: str = "bytes" - - def return_string(self) -> str: - """ How this Response should be represented as a return type """ - return self.python_type - - def constructor(self) -> str: - """ How the return value of this response should be constructed """ - return f"{self.python_type}(response.content)" +_SOURCE_BY_CONTENT_TYPE = { + "application/json": "response.json()", + "application/octet-stream": "response.content", + "text/html": "response.text", +} -openapi_types_to_python_type_strings = { - "string": "str", - "number": "float", - "integer": "int", - "boolean": "bool", -} +def empty_response(status_code: int, response_name: str) -> Response: + return Response( + status_code=status_code, + prop=NoneProperty(name=response_name, default=None, nullable=False, required=True), + source="None", + ) -def response_from_data(*, status_code: int, data: Union[oai.Response, oai.Reference]) -> Union[Response, ParseError]: +def response_from_data( + *, status_code: int, data: Union[oai.Response, oai.Reference], schemas: Schemas +) -> Tuple[Union[Response, ParseError], Schemas]: """ Generate a Response from the OpenAPI dictionary representation of it """ + response_name = f"response_{status_code}" if isinstance(data, oai.Reference) or data.content is None: - return Response(status_code=status_code) + return ( + empty_response(status_code, response_name), + schemas, + ) content = data.content - schema_data = None - if "application/json" in content: - schema_data = data.content["application/json"].media_type_schema - elif "application/octet-stream" in content: - return BytesResponse(status_code=status_code) - elif "text/html" in content: - schema_data = data.content["text/html"].media_type_schema + for content_type, media_type in content.items(): + if content_type in _SOURCE_BY_CONTENT_TYPE: + source = _SOURCE_BY_CONTENT_TYPE[content_type] + schema_data = media_type.media_type_schema + break + else: + return ParseError(data=data, detail=f"Unsupported content_type {content}"), schemas if schema_data is None: - return ParseError(data=data, detail=f"Unsupported content_type {content}") - - if isinstance(schema_data, oai.Reference): - return RefResponse( - status_code=status_code, - reference=Reference.from_ref(schema_data.ref), + return ( + empty_response(status_code, response_name), + schemas, ) - response_type = schema_data.type - if response_type is None: - return Response(status_code=status_code) - if response_type == "array" and isinstance(schema_data.items, oai.Reference): - return ListRefResponse( - status_code=status_code, - reference=Reference.from_ref(schema_data.items.ref), - ) - if ( - response_type == "array" - and isinstance(schema_data.items, oai.Schema) - and schema_data.items.type in openapi_types_to_python_type_strings - ): - return ListBasicResponse(status_code=status_code, openapi_type=schema_data.items.type) - if response_type in openapi_types_to_python_type_strings: - return BasicResponse(status_code=status_code, openapi_type=response_type) - return ParseError(data=data, detail=f"Unrecognized type {schema_data.type}") + + prop, schemas = property_from_data(name=response_name, required=True, data=schema_data, schemas=schemas) + + if isinstance(prop, PropertyError): + return prop, schemas + + return Response(status_code=status_code, prop=prop, source=source), schemas diff --git a/openapi_python_client/templates/endpoint_macros.pyi b/openapi_python_client/templates/endpoint_macros.pyi index fcfad4981..5819714d8 100644 --- a/openapi_python_client/templates/endpoint_macros.pyi +++ b/openapi_python_client/templates/endpoint_macros.pyi @@ -59,11 +59,11 @@ if {{ property.python_name }} is not UNSET: {% if endpoint.responses | length == 0 %} None {%- elif endpoint.responses | length == 1 %} -{{ endpoint.responses[0].return_string() }} +{{ endpoint.responses[0].prop.get_type_string() }} {%- else %} Union[ {% for response in endpoint.responses %} - {{ response.return_string() }}{{ "," if not loop.last }} + {{ response.prop.get_type_string() }}{{ "," if not loop.last }} {% endfor %} ] {%- endif %} diff --git a/openapi_python_client/templates/endpoint_module.pyi b/openapi_python_client/templates/endpoint_module.pyi index e0da634f5..29dfadc46 100644 --- a/openapi_python_client/templates/endpoint_module.pyi +++ b/openapi_python_client/templates/endpoint_module.pyi @@ -57,7 +57,13 @@ def _get_kwargs( def _parse_response(*, response: httpx.Response) -> Optional[{{ return_string }}]: {% for response in endpoint.responses %} if response.status_code == {{ response.status_code }}: - return {{ response.constructor() }} + {% if response.prop.template %} + {% from "property_templates/" + response.prop.template import construct %} + {{ construct(response.prop, response.source) | indent(8) }} + {% else %} + {{ response.prop.python_name }} = {{ response.source }} + {% endif %} + return {{ response.prop.python_name }} {% endfor %} return None {% endif %} diff --git a/openapi_python_client/templates/property_templates/file_property.pyi b/openapi_python_client/templates/property_templates/file_property.pyi index a66e81bd0..f759de4a6 100644 --- a/openapi_python_client/templates/property_templates/file_property.pyi +++ b/openapi_python_client/templates/property_templates/file_property.pyi @@ -1,6 +1,7 @@ {% macro construct(property, source) %} -{# Receiving files not supported (yet) #} -{{ property.python_name }} = {{ source }} +{{ property.python_name }} = File( + payload = BytesIO({{ source }}) +) {% endmacro %} {% macro transform(property, source, destination) %} diff --git a/openapi_python_client/templates/property_templates/list_property.pyi b/openapi_python_client/templates/property_templates/list_property.pyi index f0ad1f0b3..91d29c37a 100644 --- a/openapi_python_client/templates/property_templates/list_property.pyi +++ b/openapi_python_client/templates/property_templates/list_property.pyi @@ -12,7 +12,7 @@ for {{ inner_source }} in ({{ source }} or []): {{ construct(inner_property, inner_source) | indent(4) }} {{ property.python_name }}.append({{ inner_property.python_name }}) {% else %} -{{ property.python_name }} = {{ source }} +{{ property.python_name }} = cast({{ property.get_type_string(no_optional=True) }}, {{ source }}) {% endif %} {% endmacro %} diff --git a/openapi_python_client/templates/property_templates/none_property.pyi b/openapi_python_client/templates/property_templates/none_property.pyi new file mode 100644 index 000000000..300632e3c --- /dev/null +++ b/openapi_python_client/templates/property_templates/none_property.pyi @@ -0,0 +1,7 @@ +{% macro construct(property, source) %} +{{ property.python_name }} = None +{% endmacro %} + +{% macro transform(property, source, destination) %} +{{ destination }} = None +{% endmacro %} diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index 178ba2740..6df75bc70 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -245,8 +245,9 @@ def test_add_body_happy(self, mocker): assert endpoint.multipart_body_reference == multipart_body_reference def test__add_responses_error(self, mocker): - from openapi_python_client.parser.openapi import Endpoint + from openapi_python_client.parser.openapi import Endpoint, Schemas + schemas = Schemas() response_1_data = mocker.MagicMock() response_2_data = mocker.MagicMock() data = { @@ -263,12 +264,15 @@ def test__add_responses_error(self, mocker): relative_imports={"import_3"}, ) parse_error = ParseError(data=mocker.MagicMock()) - response_from_data = mocker.patch(f"{MODULE_NAME}.response_from_data", return_value=parse_error) + response_from_data = mocker.patch(f"{MODULE_NAME}.response_from_data", return_value=(parse_error, schemas)) - response = Endpoint._add_responses(endpoint, data) + response, schemas = Endpoint._add_responses(endpoint=endpoint, data=data, schemas=schemas) response_from_data.assert_has_calls( - [mocker.call(status_code=200, data=response_1_data), mocker.call(status_code=404, data=response_2_data)] + [ + mocker.call(status_code=200, data=response_1_data, schemas=schemas), + mocker.call(status_code=404, data=response_2_data, schemas=schemas), + ] ) assert response.errors == [ ParseError( @@ -282,7 +286,8 @@ def test__add_responses_error(self, mocker): ] def test__add_responses(self, mocker): - from openapi_python_client.parser.openapi import Endpoint, Reference, RefResponse + from openapi_python_client.parser.openapi import Endpoint, Response + from openapi_python_client.parser.properties import DateProperty, DateTimeProperty response_1_data = mocker.MagicMock() response_2_data = mocker.MagicMock() @@ -299,31 +304,39 @@ def test__add_responses(self, mocker): tag="tag", relative_imports={"import_3"}, ) - ref_1 = Reference.from_ref(ref="ref_1") - ref_2 = Reference.from_ref(ref="ref_2") - response_1 = RefResponse(status_code=200, reference=ref_1) - response_2 = RefResponse(status_code=404, reference=ref_2) - response_from_data = mocker.patch(f"{MODULE_NAME}.response_from_data", side_effect=[response_1, response_2]) - import_string_from_reference = mocker.patch( - f"{MODULE_NAME}.import_string_from_reference", side_effect=["import_1", "import_2"] + schemas = mocker.MagicMock() + schemas_1 = mocker.MagicMock() + schemas_2 = mocker.MagicMock() + response_1 = Response( + status_code=200, + source="source", + prop=DateTimeProperty(name="datetime", required=True, nullable=False, default=None), + ) + response_2 = Response( + status_code=404, + source="source", + prop=DateProperty(name="date", required=True, nullable=False, default=None), + ) + response_from_data = mocker.patch( + f"{MODULE_NAME}.response_from_data", side_effect=[(response_1, schemas_1), (response_2, schemas_2)] ) - endpoint = Endpoint._add_responses(endpoint, data) + endpoint, response_schemas = Endpoint._add_responses(endpoint=endpoint, data=data, schemas=schemas) response_from_data.assert_has_calls( [ - mocker.call(status_code=200, data=response_1_data), - mocker.call(status_code=404, data=response_2_data), - ] - ) - import_string_from_reference.assert_has_calls( - [ - mocker.call(ref_1, prefix="...models"), - mocker.call(ref_2, prefix="...models"), + mocker.call(status_code=200, data=response_1_data, schemas=schemas), + mocker.call(status_code=404, data=response_2_data, schemas=schemas_1), ] ) assert endpoint.responses == [response_1, response_2] - assert endpoint.relative_imports == {"import_1", "import_2", "import_3"} + assert endpoint.relative_imports == { + "from dateutil.parser import isoparse", + "from typing import cast", + "import datetime", + "import_3", + } + assert response_schemas == schemas_2 def test__add_parameters_handles_no_params(self): from openapi_python_client.parser.openapi import Endpoint, Schemas @@ -486,7 +499,8 @@ def test_from_data_bad_responses(self, mocker): _add_parameters = mocker.patch.object( Endpoint, "_add_parameters", return_value=(mocker.MagicMock(), param_schemas) ) - _add_responses = mocker.patch.object(Endpoint, "_add_responses", return_value=parse_error) + response_schemas = mocker.MagicMock() + _add_responses = mocker.patch.object(Endpoint, "_add_responses", return_value=(parse_error, response_schemas)) data = oai.Operation.construct( description=mocker.MagicMock(), operationId=mocker.MagicMock(), @@ -497,7 +511,7 @@ def test_from_data_bad_responses(self, mocker): result = Endpoint.from_data(data=data, path=path, method=method, tag="default", schemas=initial_schemas) - assert result == (parse_error, param_schemas) + assert result == (parse_error, response_schemas) def test_from_data_standard(self, mocker): from openapi_python_client.parser.openapi import Endpoint @@ -507,7 +521,11 @@ def test_from_data_standard(self, mocker): param_schemas = mocker.MagicMock() param_endpoint = mocker.MagicMock() _add_parameters = mocker.patch.object(Endpoint, "_add_parameters", return_value=(param_endpoint, param_schemas)) - _add_responses = mocker.patch.object(Endpoint, "_add_responses") + response_schemas = mocker.MagicMock() + response_endpoint = mocker.MagicMock() + _add_responses = mocker.patch.object( + Endpoint, "_add_responses", return_value=(response_endpoint, response_schemas) + ) body_schemas = mocker.MagicMock() body_endpoint = mocker.MagicMock() _add_body = mocker.patch.object(Endpoint, "_add_body", return_value=(body_endpoint, body_schemas)) @@ -537,8 +555,8 @@ def test_from_data_standard(self, mocker): data=data, schemas=initial_schemas, ) - _add_responses.assert_called_once_with(param_endpoint, data.responses) - _add_body.assert_called_once_with(endpoint=_add_responses.return_value, data=data, schemas=param_schemas) + _add_responses.assert_called_once_with(endpoint=param_endpoint, data=data.responses, schemas=param_schemas) + _add_body.assert_called_once_with(endpoint=response_endpoint, data=data, schemas=response_schemas) def test_from_data_no_operation_id(self, mocker): from openapi_python_client.parser.openapi import Endpoint @@ -548,7 +566,9 @@ def test_from_data_no_operation_id(self, mocker): _add_parameters = mocker.patch.object( Endpoint, "_add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock()) ) - _add_responses = mocker.patch.object(Endpoint, "_add_responses") + _add_responses = mocker.patch.object( + Endpoint, "_add_responses", return_value=(mocker.MagicMock(), mocker.MagicMock()) + ) _add_body = mocker.patch.object(Endpoint, "_add_body", return_value=(mocker.MagicMock(), mocker.MagicMock())) data = oai.Operation.construct( description=mocker.MagicMock(), @@ -575,9 +595,11 @@ def test_from_data_no_operation_id(self, mocker): data=data, schemas=schemas, ) - _add_responses.assert_called_once_with(_add_parameters.return_value[0], data.responses) + _add_responses.assert_called_once_with( + endpoint=_add_parameters.return_value[0], data=data.responses, schemas=_add_parameters.return_value[1] + ) _add_body.assert_called_once_with( - endpoint=_add_responses.return_value, data=data, schemas=_add_parameters.return_value[1] + endpoint=_add_responses.return_value[0], data=data, schemas=_add_responses.return_value[1] ) def test_from_data_no_security(self, mocker): @@ -592,7 +614,9 @@ def test_from_data_no_security(self, mocker): _add_parameters = mocker.patch.object( Endpoint, "_add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock()) ) - _add_responses = mocker.patch.object(Endpoint, "_add_responses") + _add_responses = mocker.patch.object( + Endpoint, "_add_responses", return_value=(mocker.MagicMock(), mocker.MagicMock()) + ) _add_body = mocker.patch.object(Endpoint, "_add_body", return_value=(mocker.MagicMock(), mocker.MagicMock())) path = mocker.MagicMock() method = mocker.MagicMock() @@ -613,9 +637,11 @@ def test_from_data_no_security(self, mocker): data=data, schemas=schemas, ) - _add_responses.assert_called_once_with(_add_parameters.return_value[0], data.responses) + _add_responses.assert_called_once_with( + endpoint=_add_parameters.return_value[0], data=data.responses, schemas=_add_parameters.return_value[1] + ) _add_body.assert_called_once_with( - endpoint=_add_responses.return_value, data=data, schemas=_add_parameters.return_value[1] + endpoint=_add_responses.return_value[0], data=data, schemas=_add_responses.return_value[1] ) diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 6ff594ac5..03731f143 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -154,11 +154,13 @@ def test_get_imports(self): prefix = "..." p = FileProperty(name="test", required=True, default=None, nullable=False) assert p.get_imports(prefix=prefix) == { + "from io import BytesIO", "from ...types import File", } p = FileProperty(name="test", required=False, default=None, nullable=False) assert p.get_imports(prefix=prefix) == { + "from io import BytesIO", "from ...types import File", "from typing import Union", "from ...types import UNSET, Unset", @@ -166,6 +168,7 @@ def test_get_imports(self): p = FileProperty(name="test", required=False, default=None, nullable=True) assert p.get_imports(prefix=prefix) == { + "from io import BytesIO", "from ...types import File", "from typing import Union", "from typing import Optional", @@ -209,13 +212,13 @@ def test_get_type_imports(self, mocker): assert p.get_imports(prefix=prefix) == { inner_import, - "from typing import List", + "from typing import cast, List", } p = ListProperty(name="test", required=False, default=None, inner_property=inner_property, nullable=False) assert p.get_imports(prefix=prefix) == { inner_import, - "from typing import List", + "from typing import cast, List", "from typing import Union", "from ...types import UNSET, Unset", } @@ -223,7 +226,7 @@ def test_get_type_imports(self, mocker): p = ListProperty(name="test", required=False, default=None, inner_property=inner_property, nullable=True) assert p.get_imports(prefix=prefix) == { inner_import, - "from typing import List", + "from typing import cast, List", "from typing import Union", "from typing import Optional", "from ...types import UNSET, Unset", @@ -718,15 +721,14 @@ def test_property_from_data_unsupported_type(self, mocker): ) def test_property_from_data_no_valid_props_in_data(self): - from openapi_python_client.parser.errors import PropertyError - from openapi_python_client.parser.properties import Schemas, property_from_data + from openapi_python_client.parser.properties import NoneProperty, Schemas, property_from_data schemas = Schemas() data = oai.Schema() - err, new_schemas = property_from_data(name="blah", required=True, data=data, schemas=schemas) + prop, new_schemas = property_from_data(name="blah", required=True, data=data, schemas=schemas) - assert err == PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined.") + assert prop == NoneProperty(name="blah", required=True, nullable=False, default=None) assert new_schemas == schemas def test_property_from_data_validation_error(self, mocker): @@ -838,16 +840,16 @@ def test_property_from_data_union(self, mocker): assert s == Schemas() def test_property_from_data_union_bad_type(self, mocker): - name = mocker.MagicMock() + name = "bad_union" required = mocker.MagicMock() - data = oai.Schema(anyOf=[{}]) + data = oai.Schema(anyOf=[{"type": "garbage"}]) mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) from openapi_python_client.parser.properties import Schemas, property_from_data p, s = property_from_data(name=name, required=required, data=data, schemas=Schemas()) - assert p == PropertyError(detail=f"Invalid property in union {name}", data=oai.Schema()) + assert p == PropertyError(detail=f"Invalid property in union {name}", data=oai.Schema(type="garbage")) class TestStringBasedProperty: @@ -934,94 +936,23 @@ def test__string_based_property_unsupported_format(self, mocker): assert p == StringProperty(name=name, required=required, nullable=True, default=None) -# def test_model_from_data(mocker): -# from openapi_python_client.parser.properties import Property -# -# in_data = oai.Schema.construct( -# title=mocker.MagicMock(), -# description=mocker.MagicMock(), -# required=["RequiredEnum"], -# properties={ -# "RequiredEnum": mocker.MagicMock(), -# "OptionalDateTime": mocker.MagicMock(), -# }, -# ) -# required_property = mocker.MagicMock(autospec=Property) -# required_imports = mocker.MagicMock() -# required_property.get_imports.return_value = {required_imports} -# optional_property = mocker.MagicMock(autospec=Property) -# optional_imports = mocker.MagicMock() -# optional_property.get_imports.return_value = {optional_imports} -# property_from_data = mocker.patch( -# f"{MODULE_NAME}.property_from_data", -# side_effect=[required_property, optional_property], -# ) -# from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") -# -# from openapi_python_client.parser.properties import model_from_data -# -# result = model_from_data(data=in_data, name=mocker.MagicMock()) -# -# from_ref.assert_called_once_with(in_data.title) -# property_from_data.assert_has_calls( -# [ -# mocker.call(name="RequiredEnum", required=True, data=in_data.properties["RequiredEnum"]), -# mocker.call(name="OptionalDateTime", required=False, data=in_data.properties["OptionalDateTime"]), -# ] -# ) -# required_property.get_imports.assert_called_once_with(prefix="..") -# optional_property.get_imports.assert_called_once_with(prefix="..") -# assert result == Model( -# reference=from_ref(), -# required_properties=[required_property], -# optional_properties=[optional_property], -# relative_imports={ -# required_imports, -# optional_imports, -# }, -# description=in_data.description, -# ) -# -# -# def test_model_from_data_property_parse_error(mocker): -# in_data = oai.Schema.construct( -# title=mocker.MagicMock(), -# description=mocker.MagicMock(), -# required=["RequiredEnum"], -# properties={ -# "RequiredEnum": mocker.MagicMock(), -# "OptionalDateTime": mocker.MagicMock(), -# }, -# ) -# parse_error = ParseError(data=mocker.MagicMock()) -# property_from_data = mocker.patch( -# f"{MODULE_NAME}.property_from_data", -# return_value=parse_error, -# ) -# from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") -# -# from openapi_python_client.parser.model import model_from_data -# -# result = model_from_data(data=in_data, name=mocker.MagicMock()) -# -# from_ref.assert_called_once_with(in_data.title) -# property_from_data.assert_called_once_with( -# name="RequiredEnum", required=True, data=in_data.properties["RequiredEnum"] -# ) -# -# assert result == parse_error - - def test_build_schemas(mocker): build_model_property = mocker.patch(f"{MODULE_NAME}.build_model_property") in_data = {"1": mocker.MagicMock(enum=None), "2": mocker.MagicMock(enum=None), "3": mocker.MagicMock(enum=None)} model_1 = mocker.MagicMock() schemas_1 = mocker.MagicMock() model_2 = mocker.MagicMock() - schemas_2 = mocker.MagicMock() + schemas_2 = mocker.MagicMock(errors=[]) error = PropertyError() - schemas_3 = mocker.MagicMock(errors=[]) - build_model_property.side_effect = [(model_1, schemas_1), (model_2, schemas_2), (error, schemas_3)] + schemas_3 = mocker.MagicMock() + + # This loops through one for each, then again to retry the error + build_model_property.side_effect = [ + (model_1, schemas_1), + (model_2, schemas_2), + (error, schemas_3), + (error, schemas_3), + ] from openapi_python_client.parser.properties import Schemas, build_schemas @@ -1032,9 +963,11 @@ def test_build_schemas(mocker): mocker.call(data=in_data["1"], name="1", schemas=Schemas(), required=True), mocker.call(data=in_data["2"], name="2", schemas=schemas_1, required=True), mocker.call(data=in_data["3"], name="3", schemas=schemas_2, required=True), + mocker.call(data=in_data["3"], name="3", schemas=schemas_2, required=True), ] ) - assert result == schemas_3 + # schemas_3 was the last to come back from build_model_property, but it should be ignored because it's an error + assert result == schemas_2 assert result.errors == [error] diff --git a/tests/test_parser/test_responses.py b/tests/test_parser/test_responses.py index ce93efb15..20e5adc7b 100644 --- a/tests/test_parser/test_responses.py +++ b/tests/test_parser/test_responses.py @@ -2,285 +2,4 @@ MODULE_NAME = "openapi_python_client.parser.responses" - -class TestResponse: - def test_return_string(self): - from openapi_python_client.parser.responses import Response - - r = Response(200) - - assert r.return_string() == "None" - - def test_constructor(self): - from openapi_python_client.parser.responses import Response - - r = Response(200) - - assert r.constructor() == "None" - - -class TestListRefResponse: - def test_return_string(self, mocker): - from openapi_python_client.parser.responses import ListRefResponse - - r = ListRefResponse(200, reference=mocker.MagicMock(class_name="SuperCoolClass")) - - assert r.return_string() == "List[SuperCoolClass]" - - def test_constructor(self, mocker): - from openapi_python_client.parser.responses import ListRefResponse - - r = ListRefResponse(200, reference=mocker.MagicMock(class_name="SuperCoolClass")) - - assert ( - r.constructor() - == "[SuperCoolClass.from_dict(item) for item in cast(List[Dict[str, Any]], response.json())]" - ) - - -class TestRefResponse: - def test_return_string(self, mocker): - from openapi_python_client.parser.responses import RefResponse - - r = RefResponse(200, reference=mocker.MagicMock(class_name="SuperCoolClass")) - - assert r.return_string() == "SuperCoolClass" - - def test_constructor(self, mocker): - from openapi_python_client.parser.responses import RefResponse - - r = RefResponse(200, reference=mocker.MagicMock(class_name="SuperCoolClass")) - - assert r.constructor() == "SuperCoolClass.from_dict(cast(Dict[str, Any], response.json()))" - - -class TestListBasicResponse: - def test_return_string(self): - from openapi_python_client.parser.responses import ListBasicResponse - - r = ListBasicResponse(200, "string") - - assert r.return_string() == "List[str]" - - r = ListBasicResponse(200, "number") - - assert r.return_string() == "List[float]" - - r = ListBasicResponse(200, "integer") - - assert r.return_string() == "List[int]" - - r = ListBasicResponse(200, "boolean") - - assert r.return_string() == "List[bool]" - - def test_constructor(self): - from openapi_python_client.parser.responses import ListBasicResponse - - r = ListBasicResponse(200, "string") - - assert r.constructor() == "[str(item) for item in cast(List[str], response.json())]" - - r = ListBasicResponse(200, "number") - - assert r.constructor() == "[float(item) for item in cast(List[float], response.json())]" - - r = ListBasicResponse(200, "integer") - - assert r.constructor() == "[int(item) for item in cast(List[int], response.json())]" - - r = ListBasicResponse(200, "boolean") - - assert r.constructor() == "[bool(item) for item in cast(List[bool], response.json())]" - - -class TestBasicResponse: - def test_return_string(self): - from openapi_python_client.parser.responses import BasicResponse - - r = BasicResponse(200, "string") - - assert r.return_string() == "str" - - r = BasicResponse(200, "number") - - assert r.return_string() == "float" - - r = BasicResponse(200, "integer") - - assert r.return_string() == "int" - - r = BasicResponse(200, "boolean") - - assert r.return_string() == "bool" - - def test_constructor(self): - from openapi_python_client.parser.responses import BasicResponse - - r = BasicResponse(200, "string") - - assert r.constructor() == "str(response.text)" - - r = BasicResponse(200, "number") - - assert r.constructor() == "float(response.text)" - - r = BasicResponse(200, "integer") - - assert r.constructor() == "int(response.text)" - - r = BasicResponse(200, "boolean") - - assert r.constructor() == "bool(response.text)" - - -class TestBytesResponse: - def test_return_string(self): - from openapi_python_client.parser.responses import BytesResponse - - b = BytesResponse(200) - - assert b.return_string() == "bytes" - - def test_constructor(self): - from openapi_python_client.parser.responses import BytesResponse - - b = BytesResponse(200) - - assert b.constructor() == "bytes(response.content)" - - -class TestResponseFromData: - def test_response_from_data_no_content(self, mocker): - from openapi_python_client.parser.responses import response_from_data - - Response = mocker.patch(f"{MODULE_NAME}.Response") - - status_code = mocker.MagicMock(autospec=int) - response = response_from_data(status_code=status_code, data=oai.Response.construct()) - - Response.assert_called_once_with(status_code=status_code) - assert response == Response() - - def test_response_from_data_unsupported_content_type(self): - from openapi_python_client.parser.errors import ParseError - from openapi_python_client.parser.responses import response_from_data - - content = {"not/real": {}} - data = oai.Response.construct(content=content) - - assert response_from_data(status_code=200, data=data) == ParseError( - data=data, detail=f"Unsupported content_type {content}" - ) - - def test_response_from_data_ref(self, mocker): - ref = mocker.MagicMock() - status_code = mocker.MagicMock(autospec=int) - data = oai.Response.construct( - content={"application/json": oai.MediaType.construct(media_type_schema=oai.Reference.construct(ref=ref))} - ) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") - RefResponse = mocker.patch(f"{MODULE_NAME}.RefResponse") - from openapi_python_client.parser.responses import response_from_data - - response = response_from_data(status_code=status_code, data=data) - - from_ref.assert_called_once_with(ref) - RefResponse.assert_called_once_with(status_code=status_code, reference=from_ref()) - assert response == RefResponse() - - def test_response_from_data_empty(self, mocker): - status_code = mocker.MagicMock(autospec=int) - data = oai.Response.construct() - Response = mocker.patch(f"{MODULE_NAME}.Response") - from openapi_python_client.parser.responses import response_from_data - - response = response_from_data(status_code=status_code, data=data) - - Response.assert_called_once_with(status_code=status_code) - assert response == Response() - - def test_response_from_data_no_response_type(self, mocker): - status_code = mocker.MagicMock(autospec=int) - data = oai.Response.construct( - content={"application/json": oai.MediaType.construct(media_type_schema=oai.Schema.construct(type=None))} - ) - Response = mocker.patch(f"{MODULE_NAME}.Response") - from openapi_python_client.parser.responses import response_from_data - - response = response_from_data(status_code=status_code, data=data) - - Response.assert_called_once_with(status_code=status_code) - assert response == Response() - - def test_response_from_data_array(self, mocker): - ref = mocker.MagicMock() - status_code = mocker.MagicMock(autospec=int) - data = oai.Response.construct( - content={ - "application/json": oai.MediaType.construct( - media_type_schema=oai.Schema.construct(type="array", items=oai.Reference.construct(ref=ref)) - ) - } - ) - from_ref = mocker.patch(f"{MODULE_NAME}.Reference.from_ref") - ListRefResponse = mocker.patch(f"{MODULE_NAME}.ListRefResponse") - from openapi_python_client.parser.responses import response_from_data - - response = response_from_data(status_code=status_code, data=data) - - from_ref.assert_called_once_with(ref) - ListRefResponse.assert_called_once_with(status_code=status_code, reference=from_ref()) - assert response == ListRefResponse() - - def test_response_from_basic_array(self, mocker): - status_code = mocker.MagicMock(autospec=int) - data = oai.Response.construct( - content={ - "application/json": oai.MediaType.construct( - media_type_schema=oai.Schema.construct(type="array", items=oai.Schema.construct(type="string")) - ) - } - ) - ListBasicResponse = mocker.patch(f"{MODULE_NAME}.ListBasicResponse") - from openapi_python_client.parser.responses import response_from_data - - response = response_from_data(status_code=status_code, data=data) - - ListBasicResponse.assert_called_once_with(status_code=status_code, openapi_type="string") - assert response == ListBasicResponse.return_value - - def test_response_from_data_basic(self, mocker): - status_code = mocker.MagicMock(autospec=int) - data = oai.Response.construct( - content={"text/html": oai.MediaType.construct(media_type_schema=oai.Schema.construct(type="string"))} - ) - BasicResponse = mocker.patch(f"{MODULE_NAME}.BasicResponse") - from openapi_python_client.parser.responses import response_from_data - - response = response_from_data(status_code=status_code, data=data) - - BasicResponse.assert_called_once_with(status_code=status_code, openapi_type="string") - assert response == BasicResponse.return_value - - def test_response_from_dict_unsupported_type(self): - from openapi_python_client.parser.errors import ParseError - from openapi_python_client.parser.responses import response_from_data - - data = oai.Response.construct( - content={"text/html": oai.MediaType.construct(media_type_schema=oai.Schema.construct(type="BLAH"))} - ) - - assert response_from_data(status_code=200, data=data) == ParseError(data=data, detail="Unrecognized type BLAH") - - def test_response_from_data_octet_stream(self, mocker): - status_code = mocker.MagicMock(autospec=int) - data = oai.Response.construct( - content={"application/octet-stream": oai.MediaType.construct(media_type_schema=mocker.MagicMock())} - ) - BytesResponse = mocker.patch(f"{MODULE_NAME}.BytesResponse") - from openapi_python_client.parser.responses import response_from_data - - response = response_from_data(status_code=status_code, data=data) - - assert response == BytesResponse() +# TODO: Test response_from_data diff --git a/tests/test_templates/endpoint_module.py b/tests/test_templates/endpoint_module.py index b585e77e5..def57a78f 100644 --- a/tests/test_templates/endpoint_module.py +++ b/tests/test_templates/endpoint_module.py @@ -32,19 +32,17 @@ def _get_kwargs( } -def _parse_response( - *, response: httpx.Response -) -> Optional[Union[str, int,]]: +def _parse_response(*, response: httpx.Response) -> Optional[Union[str, int]]: if response.status_code == 200: - return str(response.text) + response_one = response.json() + return response_one if response.status_code == 201: - return int(response.text) + response_one = response.json() + return response_one return None -def _build_response( - *, response: httpx.Response -) -> Response[Union[str, int,]]: +def _build_response(*, response: httpx.Response) -> Response[Union[str, int]]: return Response( status_code=response.status_code, content=response.content, @@ -59,7 +57,7 @@ def sync_detailed( form_data: FormBody, multipart_data: MultiPartBody, json_body: Json, -) -> Response[Union[str, int,]]: +) -> Response[Union[str, int]]: kwargs = _get_kwargs( client=client, form_data=form_data, @@ -80,7 +78,7 @@ def sync( form_data: FormBody, multipart_data: MultiPartBody, json_body: Json, -) -> Optional[Union[str, int,]]: +) -> Optional[Union[str, int]]: """ POST endpoint """ return sync_detailed( @@ -97,7 +95,7 @@ async def asyncio_detailed( form_data: FormBody, multipart_data: MultiPartBody, json_body: Json, -) -> Response[Union[str, int,]]: +) -> Response[Union[str, int]]: kwargs = _get_kwargs( client=client, form_data=form_data, @@ -117,7 +115,7 @@ async def asyncio( form_data: FormBody, multipart_data: MultiPartBody, json_body: Json, -) -> Optional[Union[str, int,]]: +) -> Optional[Union[str, int]]: """ POST endpoint """ return ( diff --git a/tests/test_templates/test_endpoint_module.py b/tests/test_templates/test_endpoint_module.py index bc877d20a..082ca84b3 100644 --- a/tests/test_templates/test_endpoint_module.py +++ b/tests/test_templates/test_endpoint_module.py @@ -24,12 +24,14 @@ def test_async_module(template, mocker): multipart_body_reference = mocker.MagicMock(class_name="MultiPartBody") json_body = mocker.MagicMock(template=None, python_name="json_body") json_body.get_type_string.return_value = "Json" - post_response_1 = mocker.MagicMock(status_code=200) - post_response_1.return_string.return_value = "str" - post_response_1.constructor.return_value = "str(response.text)" - post_response_2 = mocker.MagicMock(status_code=201) - post_response_2.return_string.return_value = "int" - post_response_2.constructor.return_value = "int(response.text)" + post_response_1 = mocker.MagicMock( + status_code=200, source="response.json()", prop=mocker.MagicMock(template=None, python_name="response_one") + ) + post_response_1.prop.get_type_string.return_value = "str" + post_response_2 = mocker.MagicMock( + status_code=201, source="response.json()", prop=mocker.MagicMock(template=None, python_name="response_one") + ) + post_response_2.prop.get_type_string.return_value = "int" post_endpoint = mocker.MagicMock( name="camelCase", requires_security=True, @@ -47,6 +49,7 @@ def test_async_module(template, mocker): post_endpoint.name = "camelCase" result = template.render(endpoint=post_endpoint) + import black expected = (Path(__file__).parent / "endpoint_module.py").read_text() From f3672a17826a5ef47ece40b9a2ed4190f886e94a Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 7 Nov 2020 14:41:52 -0700 Subject: [PATCH 20/25] Fix typing in custom template e2e test --- .../custom_e2e/api/tests/defaults_tests_defaults_post.py | 8 +++++--- .../custom_e2e/api/tests/get_basic_list_of_booleans.py | 8 +++++--- .../custom_e2e/api/tests/get_basic_list_of_floats.py | 8 +++++--- .../custom_e2e/api/tests/get_basic_list_of_integers.py | 8 +++++--- .../custom_e2e/api/tests/get_basic_list_of_strings.py | 8 +++++--- .../custom_e2e/api/tests/get_user_list.py | 8 +++++--- .../custom_e2e/api/tests/int_enum_tests_int_enum_post.py | 8 +++++--- .../api/tests/json_body_tests_json_body_post.py | 8 +++++--- .../api/tests/no_response_tests_no_response_get.py | 8 +++++--- .../api/tests/octet_stream_tests_octet_stream_get.py | 8 +++++--- .../tests/optional_value_tests_optional_query_param.py | 8 +++++--- .../custom_e2e/api/tests/test_inline_objects.py | 8 +++++--- .../unsupported_content_tests_unsupported_content_get.py | 8 +++++--- .../custom_e2e/api/tests/upload_file_tests_upload_post.py | 8 +++++--- .../test_custom_templates/endpoint_module.pyi | 8 +++++--- 15 files changed, 75 insertions(+), 45 deletions(-) diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py index c40d9ae7f..18772a156 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/defaults_tests_defaults_post.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client import datetime @@ -26,8 +28,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPVal return None -def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, HTTPValidationError]]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPValidationError]]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -47,7 +49,7 @@ def httpx_request( list_prop: Union[Unset, List[AnEnum]] = UNSET, union_prop: Union[Unset, float, str] = "not a float", enum_prop: Union[Unset, AnEnum] = UNSET, -) -> httpx.Response[Union[None, HTTPValidationError]]: +) -> Response[Union[None, HTTPValidationError]]: json_datetime_prop: Union[Unset, str] = UNSET if not isinstance(datetime_prop, Unset): diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_booleans.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_booleans.py index 7011197a0..e048fb001 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_booleans.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_booleans.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client from typing import List, cast @@ -15,8 +17,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[List[bool]]: return None -def _build_response(*, response: httpx.Response) -> httpx.Response[List[bool]]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[List[bool]]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -27,7 +29,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[List[bool]]: def httpx_request( *, client: Client, -) -> httpx.Response[List[bool]]: +) -> Response[List[bool]]: response = client.request( "get", diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_floats.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_floats.py index 0af3dee86..a4e9f5fd4 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_floats.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_floats.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client from typing import List, cast @@ -15,8 +17,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[List[float]]: return None -def _build_response(*, response: httpx.Response) -> httpx.Response[List[float]]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[List[float]]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -27,7 +29,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[List[float]]: def httpx_request( *, client: Client, -) -> httpx.Response[List[float]]: +) -> Response[List[float]]: response = client.request( "get", diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_integers.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_integers.py index 76a227cd5..232a3c7bb 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_integers.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_integers.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client from typing import List, cast @@ -15,8 +17,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[List[int]]: return None -def _build_response(*, response: httpx.Response) -> httpx.Response[List[int]]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[List[int]]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -27,7 +29,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[List[int]]: def httpx_request( *, client: Client, -) -> httpx.Response[List[int]]: +) -> Response[List[int]]: response = client.request( "get", diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_strings.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_strings.py index 7205112f4..a16a16932 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_strings.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_basic_list_of_strings.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client from typing import List, cast @@ -15,8 +17,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[List[str]]: return None -def _build_response(*, response: httpx.Response) -> httpx.Response[List[str]]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[List[str]]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -27,7 +29,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[List[str]]: def httpx_request( *, client: Client, -) -> httpx.Response[List[str]]: +) -> Response[List[str]]: response = client.request( "get", diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py index 204467253..2821664fb 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/get_user_list.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client import datetime @@ -28,8 +30,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[List[AModel], return None -def _build_response(*, response: httpx.Response) -> httpx.Response[Union[List[AModel], HTTPValidationError]]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[Union[List[AModel], HTTPValidationError]]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -42,7 +44,7 @@ def httpx_request( client: Client, an_enum_value: List[AnEnum], some_date: Union[datetime.date, datetime.datetime], -) -> httpx.Response[Union[List[AModel], HTTPValidationError]]: +) -> Response[Union[List[AModel], HTTPValidationError]]: json_an_enum_value = [] for an_enum_value_item_data in an_enum_value: diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/int_enum_tests_int_enum_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/int_enum_tests_int_enum_post.py index d10cb6e9b..855a8d999 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/int_enum_tests_int_enum_post.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/int_enum_tests_int_enum_post.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client from typing import Dict @@ -22,8 +24,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPVal return None -def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, HTTPValidationError]]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPValidationError]]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -35,7 +37,7 @@ def httpx_request( *, client: Client, int_enum: AnIntEnum, -) -> httpx.Response[Union[None, HTTPValidationError]]: +) -> Response[Union[None, HTTPValidationError]]: json_int_enum = int_enum.value diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/json_body_tests_json_body_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/json_body_tests_json_body_post.py index ad8bb7c67..30f80e33d 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/json_body_tests_json_body_post.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/json_body_tests_json_body_post.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client from ...models.a_model import AModel @@ -20,8 +22,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPVal return None -def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, HTTPValidationError]]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPValidationError]]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -33,7 +35,7 @@ def httpx_request( *, client: Client, json_body: AModel, -) -> httpx.Response[Union[None, HTTPValidationError]]: +) -> Response[Union[None, HTTPValidationError]]: json_json_body = json_body.to_dict() diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/no_response_tests_no_response_get.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/no_response_tests_no_response_get.py index 4f4d89cbd..09707caf9 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/no_response_tests_no_response_get.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/no_response_tests_no_response_get.py @@ -1,10 +1,12 @@ import httpx +from ...types import Response + Client = httpx.Client -def _build_response(*, response: httpx.Response) -> httpx.Response[None]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[None]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -15,7 +17,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[None]: def httpx_request( *, client: Client, -) -> httpx.Response[None]: +) -> Response[None]: response = client.request( "get", diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/octet_stream_tests_octet_stream_get.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/octet_stream_tests_octet_stream_get.py index 1fd6e3637..409dc8a0c 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/octet_stream_tests_octet_stream_get.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/octet_stream_tests_octet_stream_get.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client from io import BytesIO @@ -17,8 +19,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[File]: return None -def _build_response(*, response: httpx.Response) -> httpx.Response[File]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[File]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -29,7 +31,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[File]: def httpx_request( *, client: Client, -) -> httpx.Response[File]: +) -> Response[File]: response = client.request( "get", diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py index 68c5e2d98..bff43cc10 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/optional_value_tests_optional_query_param.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client from typing import Dict, List, Union @@ -22,8 +24,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[None, HTTPVal return None -def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, HTTPValidationError]]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPValidationError]]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -35,7 +37,7 @@ def httpx_request( *, client: Client, query_param: Union[Unset, List[str]] = UNSET, -) -> httpx.Response[Union[None, HTTPValidationError]]: +) -> Response[Union[None, HTTPValidationError]]: json_query_param: Union[Unset, List[Any]] = UNSET if not isinstance(query_param, Unset): diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py index 4d4d867f9..8046810f2 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client from ...models.json_body import JsonBody @@ -16,8 +18,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[Response_200]: return None -def _build_response(*, response: httpx.Response) -> httpx.Response[Response_200]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[Response_200]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -29,7 +31,7 @@ def httpx_request( *, client: Client, json_body: JsonBody, -) -> httpx.Response[Response_200]: +) -> Response[Response_200]: json_json_body = json_body.to_dict() diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/unsupported_content_tests_unsupported_content_get.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/unsupported_content_tests_unsupported_content_get.py index c1019e884..333374cdb 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/unsupported_content_tests_unsupported_content_get.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/unsupported_content_tests_unsupported_content_get.py @@ -1,10 +1,12 @@ import httpx +from ...types import Response + Client = httpx.Client -def _build_response(*, response: httpx.Response) -> httpx.Response[None]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[None]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -15,7 +17,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[None]: def httpx_request( *, client: Client, -) -> httpx.Response[None]: +) -> Response[None]: response = client.request( "get", diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py index 91153fbfd..2d084a9e0 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/upload_file_tests_upload_post.py @@ -2,6 +2,8 @@ import httpx +from ...types import Response + Client = httpx.Client from typing import Dict, Union, cast @@ -27,11 +29,11 @@ def _parse_response(*, response: httpx.Response) -> Optional[Union[ -def _build_response(*, response: httpx.Response) -> httpx.Response[Union[ +def _build_response(*, response: httpx.Response) -> Response[Union[ None, HTTPValidationError ]]: - return httpx.Response( + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -43,7 +45,7 @@ def httpx_request(*, client: Client, multipart_data: BodyUploadFileTestsUploadPost, keep_alive: Union[Unset, bool] = UNSET, -) -> httpx.Response[Union[ +) -> Response[Union[ None, HTTPValidationError ]]: diff --git a/end_to_end_tests/test_custom_templates/endpoint_module.pyi b/end_to_end_tests/test_custom_templates/endpoint_module.pyi index 34b46c329..2964791eb 100644 --- a/end_to_end_tests/test_custom_templates/endpoint_module.pyi +++ b/end_to_end_tests/test_custom_templates/endpoint_module.pyi @@ -2,6 +2,8 @@ from typing import Optional import httpx +from ...types import Response + Client = httpx.Client {% for relative in endpoint.relative_imports %} @@ -31,8 +33,8 @@ def _parse_response(*, response: httpx.Response) -> Optional[{{ return_string }} -def _build_response(*, response: httpx.Response) -> httpx.Response[{{ return_string }}]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[{{ return_string }}]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -44,7 +46,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[{{ return_str ) -def httpx_request({{ arguments(endpoint) | indent(4) }}) -> httpx.Response[{{ return_string }}]: +def httpx_request({{ arguments(endpoint) | indent(4) }}) -> Response[{{ return_string }}]: {{ header_params(endpoint) | indent(4) }} {{ query_params(endpoint) | indent(4) }} {{ json_body(endpoint) | indent(4) }} From 5575eea20c264b1b0e647519eeef1f6ee328276d Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 7 Nov 2020 17:04:55 -0700 Subject: [PATCH 21/25] Remove slots from attr classes to support Python 3.6 --- .../parser/properties/__init__.py | 20 +++++++++---------- .../parser/properties/enum_property.py | 2 +- .../parser/properties/model_property.py | 2 +- .../parser/properties/property.py | 2 +- .../parser/properties/schemas.py | 2 +- openapi_python_client/parser/responses.py | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index ec63875ef..1ec697e42 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -14,7 +14,7 @@ from .schemas import Schemas -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class NoneProperty(Property): """ A property that is always None (used for empty schemas) """ @@ -22,7 +22,7 @@ class NoneProperty(Property): template: ClassVar[Optional[str]] = "none_property.pyi" -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class StringProperty(Property): """ A property of type str """ @@ -31,7 +31,7 @@ class StringProperty(Property): _type_string: ClassVar[str] = "str" -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class DateTimeProperty(Property): """ A property of type datetime.datetime @@ -53,7 +53,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: return imports -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class DateProperty(Property): """ A property of type datetime.date """ @@ -73,7 +73,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: return imports -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class FileProperty(Property): """ A property used for uploading files """ @@ -93,21 +93,21 @@ def get_imports(self, *, prefix: str) -> Set[str]: return imports -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class FloatProperty(Property): """ A property of type float """ _type_string: ClassVar[str] = "float" -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class IntProperty(Property): """ A property of type int """ _type_string: ClassVar[str] = "int" -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class BooleanProperty(Property): """ Property for bool """ @@ -117,7 +117,7 @@ class BooleanProperty(Property): InnerProp = TypeVar("InnerProp", bound=Property) -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class ListProperty(Property, Generic[InnerProp]): """ A property representing a list (array) of other properties """ @@ -149,7 +149,7 @@ def get_imports(self, *, prefix: str) -> Set[str]: return imports -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class UnionProperty(Property): """ A property representing a Union (anyOf) of other properties """ diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index be6311e42..1217f23ee 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -11,7 +11,7 @@ ValueType = Union[str, int] -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class EnumProperty(Property): """ A property that should use an enum """ diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 021f4677a..34becd00b 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -6,7 +6,7 @@ from .property import Property -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class ModelProperty(Property): """ A property which refers to another Schema """ diff --git a/openapi_python_client/parser/properties/property.py b/openapi_python_client/parser/properties/property.py index e5af34911..c7649200f 100644 --- a/openapi_python_client/parser/properties/property.py +++ b/openapi_python_client/parser/properties/property.py @@ -5,7 +5,7 @@ from ... import utils -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class Property: """ Describes a single property for a schema diff --git a/openapi_python_client/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py index 48acdf392..338938673 100644 --- a/openapi_python_client/parser/properties/schemas.py +++ b/openapi_python_client/parser/properties/schemas.py @@ -9,7 +9,7 @@ from .model_property import ModelProperty -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class Schemas: """ Structure for containing all defined, shareable, and resuabled schemas (attr classes and Enums) """ diff --git a/openapi_python_client/parser/responses.py b/openapi_python_client/parser/responses.py index 32b3a4fe2..a7c9a5e74 100644 --- a/openapi_python_client/parser/responses.py +++ b/openapi_python_client/parser/responses.py @@ -7,7 +7,7 @@ from .properties import NoneProperty, Property, Schemas, property_from_data -@attr.s(auto_attribs=True, frozen=True, slots=True) +@attr.s(auto_attribs=True, frozen=True) class Response: """ Describes a single response for an endpoint """ From fbdbd21658e8b76a6caa7b5e3a36eee16e6856b3 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 7 Nov 2020 18:01:30 -0700 Subject: [PATCH 22/25] Added improved naming scheme using parent elements --- CHANGELOG.md | 4 + .../api/tests/test_inline_objects.py | 14 +-- .../custom_e2e/models/__init__.py | 4 +- ...ody.py => test_inline_objectsjson_body.py} | 6 +- .../test_inline_objectsresponse_200.py} | 6 +- .../api/tests/test_inline_objects.py | 28 ++--- .../my_test_api_client/models/__init__.py | 4 +- .../models/test_inline_objectsjson_body.py} | 6 +- ....py => test_inline_objectsresponse_200.py} | 6 +- openapi_python_client/parser/openapi.py | 24 +++- .../parser/properties/__init__.py | 85 ++++++++++---- openapi_python_client/parser/responses.py | 10 +- tests/test_parser/test_openapi.py | 30 +++-- .../test_parser/test_properties/test_init.py | 108 ++++++++++-------- 14 files changed, 213 insertions(+), 122 deletions(-) rename end_to_end_tests/golden-record-custom/custom_e2e/models/{json_body.py => test_inline_objectsjson_body.py} (72%) rename end_to_end_tests/{golden-record/my_test_api_client/models/json_body.py => golden-record-custom/custom_e2e/models/test_inline_objectsresponse_200.py} (71%) rename end_to_end_tests/{golden-record-custom/custom_e2e/models/response_200.py => golden-record/my_test_api_client/models/test_inline_objectsjson_body.py} (72%) rename end_to_end_tests/golden-record/my_test_api_client/models/{response_200.py => test_inline_objectsresponse_200.py} (71%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cabe053c..8d52a0ae9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Response schema handling was unified with input schema handling, meaning that responses will behave differently than before. Specifically, instead of the content-type deciding what the generated Python type is, the schema itself will. - Instead of skipping input properties with no type, enum, anyOf, or oneOf declared, the property will be declared as `None`. +- Class (models and Enums) names will now contain the name of their parent element (if any). For example, a property + declared in an endpoint will be named like {endpoint_name}_{previous_class_name}. Classes will no longer be + deduplicated by appending a number to the end of the generated name, so if two names conflict with this new naming + scheme, there will be an error instead. ### Additions diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py index 8046810f2..8dcede9cb 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py @@ -6,19 +6,19 @@ Client = httpx.Client -from ...models.json_body import JsonBody -from ...models.response_200 import Response_200 +from ...models.test_inline_objectsjson_body import TestInlineObjectsjsonBody +from ...models.test_inline_objectsresponse_200 import TestInlineObjectsresponse_200 -def _parse_response(*, response: httpx.Response) -> Optional[Response_200]: +def _parse_response(*, response: httpx.Response) -> Optional[TestInlineObjectsresponse_200]: if response.status_code == 200: - response_200 = Response_200.from_dict(response.json()) + response_200 = TestInlineObjectsresponse_200.from_dict(response.json()) return response_200 return None -def _build_response(*, response: httpx.Response) -> Response[Response_200]: +def _build_response(*, response: httpx.Response) -> Response[TestInlineObjectsresponse_200]: return Response( status_code=response.status_code, content=response.content, @@ -30,8 +30,8 @@ def _build_response(*, response: httpx.Response) -> Response[Response_200]: def httpx_request( *, client: Client, - json_body: JsonBody, -) -> Response[Response_200]: + json_body: TestInlineObjectsjsonBody, +) -> Response[TestInlineObjectsresponse_200]: json_json_body = json_body.to_dict() diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py index 2f0aa1f1a..5ae4c16b7 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/__init__.py @@ -6,6 +6,6 @@ from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from .different_enum import DifferentEnum from .http_validation_error import HTTPValidationError -from .json_body import JsonBody -from .response_200 import Response_200 +from .test_inline_objectsjson_body import TestInlineObjectsjsonBody +from .test_inline_objectsresponse_200 import TestInlineObjectsresponse_200 from .validation_error import ValidationError diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/json_body.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsjson_body.py similarity index 72% rename from end_to_end_tests/golden-record-custom/custom_e2e/models/json_body.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsjson_body.py index 57577c983..833d8f9a0 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/json_body.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsjson_body.py @@ -4,7 +4,7 @@ @attr.s(auto_attribs=True) -class JsonBody: +class TestInlineObjectsjsonBody: """ """ a_property: str @@ -19,9 +19,9 @@ def to_dict(self) -> Dict[str, Any]: return field_dict @staticmethod - def from_dict(d: Dict[str, Any]) -> "JsonBody": + def from_dict(d: Dict[str, Any]) -> "TestInlineObjectsjsonBody": a_property = d["a_property"] - return JsonBody( + return TestInlineObjectsjsonBody( a_property=a_property, ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsresponse_200.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/models/json_body.py rename to end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsresponse_200.py index 57577c983..ebe0a4d72 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/json_body.py +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsresponse_200.py @@ -4,7 +4,7 @@ @attr.s(auto_attribs=True) -class JsonBody: +class TestInlineObjectsresponse_200: """ """ a_property: str @@ -19,9 +19,9 @@ def to_dict(self) -> Dict[str, Any]: return field_dict @staticmethod - def from_dict(d: Dict[str, Any]) -> "JsonBody": + def from_dict(d: Dict[str, Any]) -> "TestInlineObjectsresponse_200": a_property = d["a_property"] - return JsonBody( + return TestInlineObjectsresponse_200( a_property=a_property, ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py index 07161897e..53fc2c021 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py +++ b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py @@ -3,15 +3,15 @@ import httpx from ...client import Client -from ...models.json_body import JsonBody -from ...models.response_200 import Response_200 +from ...models.test_inline_objectsjson_body import TestInlineObjectsjsonBody +from ...models.test_inline_objectsresponse_200 import TestInlineObjectsresponse_200 from ...types import Response def _get_kwargs( *, client: Client, - json_body: JsonBody, + json_body: TestInlineObjectsjsonBody, ) -> Dict[str, Any]: url = "{}/tests/inline_objects".format(client.base_url) @@ -28,15 +28,15 @@ def _get_kwargs( } -def _parse_response(*, response: httpx.Response) -> Optional[Response_200]: +def _parse_response(*, response: httpx.Response) -> Optional[TestInlineObjectsresponse_200]: if response.status_code == 200: - response_200 = Response_200.from_dict(response.json()) + response_200 = TestInlineObjectsresponse_200.from_dict(response.json()) return response_200 return None -def _build_response(*, response: httpx.Response) -> Response[Response_200]: +def _build_response(*, response: httpx.Response) -> Response[TestInlineObjectsresponse_200]: return Response( status_code=response.status_code, content=response.content, @@ -48,8 +48,8 @@ def _build_response(*, response: httpx.Response) -> Response[Response_200]: def sync_detailed( *, client: Client, - json_body: JsonBody, -) -> Response[Response_200]: + json_body: TestInlineObjectsjsonBody, +) -> Response[TestInlineObjectsresponse_200]: kwargs = _get_kwargs( client=client, json_body=json_body, @@ -65,8 +65,8 @@ def sync_detailed( def sync( *, client: Client, - json_body: JsonBody, -) -> Optional[Response_200]: + json_body: TestInlineObjectsjsonBody, +) -> Optional[TestInlineObjectsresponse_200]: """ """ return sync_detailed( @@ -78,8 +78,8 @@ def sync( async def asyncio_detailed( *, client: Client, - json_body: JsonBody, -) -> Response[Response_200]: + json_body: TestInlineObjectsjsonBody, +) -> Response[TestInlineObjectsresponse_200]: kwargs = _get_kwargs( client=client, json_body=json_body, @@ -94,8 +94,8 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - json_body: JsonBody, -) -> Optional[Response_200]: + json_body: TestInlineObjectsjsonBody, +) -> Optional[TestInlineObjectsresponse_200]: """ """ return ( 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 2f0aa1f1a..5ae4c16b7 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 @@ -6,6 +6,6 @@ from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from .different_enum import DifferentEnum from .http_validation_error import HTTPValidationError -from .json_body import JsonBody -from .response_200 import Response_200 +from .test_inline_objectsjson_body import TestInlineObjectsjsonBody +from .test_inline_objectsresponse_200 import TestInlineObjectsresponse_200 from .validation_error import ValidationError diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/response_200.py b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsjson_body.py similarity index 72% rename from end_to_end_tests/golden-record-custom/custom_e2e/models/response_200.py rename to end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsjson_body.py index 0055f6699..833d8f9a0 100644 --- a/end_to_end_tests/golden-record-custom/custom_e2e/models/response_200.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsjson_body.py @@ -4,7 +4,7 @@ @attr.s(auto_attribs=True) -class Response_200: +class TestInlineObjectsjsonBody: """ """ a_property: str @@ -19,9 +19,9 @@ def to_dict(self) -> Dict[str, Any]: return field_dict @staticmethod - def from_dict(d: Dict[str, Any]) -> "Response_200": + def from_dict(d: Dict[str, Any]) -> "TestInlineObjectsjsonBody": a_property = d["a_property"] - return Response_200( + return TestInlineObjectsjsonBody( a_property=a_property, ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/response_200.py b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsresponse_200.py similarity index 71% rename from end_to_end_tests/golden-record/my_test_api_client/models/response_200.py rename to end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsresponse_200.py index 0055f6699..ebe0a4d72 100644 --- a/end_to_end_tests/golden-record/my_test_api_client/models/response_200.py +++ b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsresponse_200.py @@ -4,7 +4,7 @@ @attr.s(auto_attribs=True) -class Response_200: +class TestInlineObjectsresponse_200: """ """ a_property: str @@ -19,9 +19,9 @@ def to_dict(self) -> Dict[str, Any]: return field_dict @staticmethod - def from_dict(d: Dict[str, Any]) -> "Response_200": + def from_dict(d: Dict[str, Any]) -> "TestInlineObjectsresponse_200": a_property = d["a_property"] - return Response_200( + return TestInlineObjectsresponse_200( a_property=a_property, ) diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index 0c42790d9..3053ee305 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -119,13 +119,19 @@ def parse_multipart_body(body: oai.RequestBody) -> Optional[Reference]: @staticmethod def parse_request_json_body( - *, body: oai.RequestBody, schemas: Schemas + *, body: oai.RequestBody, schemas: Schemas, parent_name: str ) -> Tuple[Union[Property, PropertyError, None], Schemas]: """ Return json_body """ body_content = body.content json_body = body_content.get("application/json") if json_body is not None and json_body.media_type_schema is not None: - return property_from_data("json_body", required=True, data=json_body.media_type_schema, schemas=schemas) + return property_from_data( + name="json_body", + required=True, + data=json_body.media_type_schema, + schemas=schemas, + parent_name=parent_name, + ) return None, schemas @staticmethod @@ -138,7 +144,9 @@ def _add_body( return endpoint, schemas endpoint.form_body_reference = Endpoint.parse_request_form_body(data.requestBody) - json_body, schemas = Endpoint.parse_request_json_body(body=data.requestBody, schemas=schemas) + json_body, schemas = Endpoint.parse_request_json_body( + body=data.requestBody, schemas=schemas, parent_name=endpoint.name + ) if isinstance(json_body, ParseError): return ParseError(detail=f"cannot parse body of endpoint {endpoint.name}", data=json_body.data), schemas @@ -161,7 +169,9 @@ def _add_body( def _add_responses(*, endpoint: "Endpoint", data: oai.Responses, schemas: Schemas) -> Tuple["Endpoint", Schemas]: endpoint = deepcopy(endpoint) for code, response_data in data.items(): - response, schemas = response_from_data(status_code=int(code), data=response_data, schemas=schemas) + response, schemas = response_from_data( + status_code=int(code), data=response_data, schemas=schemas, parent_name=endpoint.name + ) if isinstance(response, ParseError): endpoint.errors.append( ParseError( @@ -188,7 +198,11 @@ def _add_parameters( if isinstance(param, oai.Reference) or param.param_schema is None: continue prop, schemas = property_from_data( - name=param.name, required=param.required, data=param.param_schema, schemas=schemas + name=param.name, + required=param.required, + data=param.param_schema, + schemas=schemas, + parent_name=endpoint.name, ) if isinstance(prop, ParseError): return ParseError(detail=f"cannot parse parameter of endpoint {endpoint.name}", data=prop.data), schemas diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py index 1ec697e42..1295854fd 100644 --- a/openapi_python_client/parser/properties/__init__.py +++ b/openapi_python_client/parser/properties/__init__.py @@ -221,7 +221,7 @@ def _string_based_property( def build_model_property( - *, data: oai.Schema, name: str, schemas: Schemas, required: bool + *, data: oai.Schema, name: str, schemas: Schemas, required: bool, parent_name: Optional[str] ) -> Tuple[Union[ModelProperty, PropertyError], Schemas]: """ A single ModelProperty from its OAI data @@ -237,11 +237,16 @@ def build_model_property( optional_properties: List[Property] = [] relative_imports: Set[str] = set() - ref = Reference.from_ref(data.title or name) + class_name = data.title or name + if parent_name: + class_name = f"{utils.pascal_case(parent_name)}{class_name}" + ref = Reference.from_ref(class_name) for key, value in (data.properties or {}).items(): prop_required = key in required_set - prop, schemas = property_from_data(name=key, required=required, data=value, schemas=schemas) + prop, schemas = property_from_data( + name=key, required=required, data=value, schemas=schemas, parent_name=class_name + ) if isinstance(prop, PropertyError): return prop, schemas if prop_required and not prop.nullable: @@ -266,19 +271,44 @@ def build_model_property( def build_enum_property( - *, data: oai.Schema, name: str, required: bool, schemas: Schemas, enum: List[Union[str, int]] + *, + data: oai.Schema, + name: str, + required: bool, + schemas: Schemas, + enum: List[Union[str, int]], + parent_name: Optional[str], ) -> Tuple[Union[EnumProperty, PropertyError], Schemas]: + """ + Create an EnumProperty from schema data. - reference = Reference.from_ref(data.title or name) + Args: + data: The OpenAPI Schema which defines this enum. + name: The name to use for variables which receive this Enum's value (e.g. model property name) + required: Whether or not this Property is required in the calling context + schemas: The Schemas which have been defined so far (used to prevent naming collisions) + enum: The enum from the provided data. Required separately here to prevent extra type checking. + parent_name: The context in which this EnumProperty is defined, used to create more specific class names. + + Returns: + A tuple containing either the created property or a PropertyError describing what went wrong AND update schemas. + """ + + class_name = data.title or name + if parent_name: + class_name = f"{utils.pascal_case(parent_name)}{class_name}" + reference = Reference.from_ref(class_name) values = EnumProperty.values_from_list(enum) - dedup_counter = 0 # TODO: use the parent names instead of a counter for deduping - while reference.class_name in schemas.enums: + if reference.class_name in schemas.enums: existing = schemas.enums[reference.class_name] - if values == existing.values: - break # This is the same Enum, we're good - dedup_counter += 1 - reference = Reference.from_ref(f"{reference.class_name}{dedup_counter}") + if values != existing.values: + return ( + PropertyError( + detail=f"Found conflicting enums named {reference.class_name} with incompatible values.", data=data + ), + schemas, + ) for value in values.values(): value_type = type(value) @@ -313,11 +343,13 @@ def build_enum_property( def build_union_property( - *, data: oai.Schema, name: str, required: bool, schemas: Schemas + *, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str ) -> Tuple[Union[UnionProperty, PropertyError], Schemas]: sub_properties: List[Property] = [] for sub_prop_data in chain(data.anyOf, data.oneOf): - sub_prop, schemas = property_from_data(name=name, required=required, data=sub_prop_data, schemas=schemas) + sub_prop, schemas = property_from_data( + name=name, required=required, data=sub_prop_data, schemas=schemas, parent_name=parent_name + ) if isinstance(sub_prop, PropertyError): return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data), schemas sub_properties.append(sub_prop) @@ -336,11 +368,13 @@ def build_union_property( def build_list_property( - *, data: oai.Schema, name: str, required: bool, schemas: Schemas + *, data: oai.Schema, name: str, required: bool, schemas: Schemas, parent_name: str ) -> Tuple[Union[ListProperty[Any], PropertyError], Schemas]: if data.items is None: return PropertyError(data=data, detail="type array must have items defined"), schemas - inner_prop, schemas = property_from_data(name=f"{name}_item", required=True, data=data.items, schemas=schemas) + inner_prop, schemas = property_from_data( + name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name=f"{parent_name}_{name}" + ) if isinstance(inner_prop, PropertyError): return PropertyError(data=inner_prop.data, detail=f"invalid data in items of array {name}"), schemas return ( @@ -360,6 +394,7 @@ def _property_from_data( required: bool, data: Union[oai.Reference, oai.Schema], schemas: Schemas, + parent_name: str, ) -> Tuple[Union[Property, PropertyError], Schemas]: """ Generate a Property from the OpenAPI dictionary representation of it """ name = utils.remove_string_escapes(name) @@ -373,9 +408,11 @@ def _property_from_data( ) return PropertyError(data=data, detail="Could not find reference in parsed models or enums"), schemas if data.enum: - return build_enum_property(data=data, name=name, required=required, schemas=schemas, enum=data.enum) + return build_enum_property( + data=data, name=name, required=required, schemas=schemas, enum=data.enum, parent_name=parent_name + ) if data.anyOf or data.oneOf: - return build_union_property(data=data, name=name, required=required, schemas=schemas) + return build_union_property(data=data, name=name, required=required, schemas=schemas, parent_name=parent_name) if not data.type: return NoneProperty(name=name, required=required, nullable=False, default=None), schemas @@ -412,20 +449,22 @@ def _property_from_data( schemas, ) elif data.type == "array": - return build_list_property(data=data, name=name, required=required, schemas=schemas) + return build_list_property(data=data, name=name, required=required, schemas=schemas, parent_name=parent_name) elif data.type == "object": - return build_model_property(data=data, name=name, schemas=schemas, required=required) + return build_model_property(data=data, name=name, schemas=schemas, required=required, parent_name=parent_name) return PropertyError(data=data, detail=f"unknown type {data.type}"), schemas def property_from_data( + *, name: str, required: bool, data: Union[oai.Reference, oai.Schema], schemas: Schemas, + parent_name: str, ) -> Tuple[Union[Property, PropertyError], Schemas]: try: - return _property_from_data(name=name, required=required, data=data, schemas=schemas) + return _property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name=parent_name) except ValidationError: return PropertyError(detail="Failed to validate default value", data=data), schemas @@ -433,9 +472,11 @@ def property_from_data( def update_schemas_with_data(name: str, data: oai.Schema, schemas: Schemas) -> Union[Schemas, PropertyError]: prop: Union[PropertyError, ModelProperty, EnumProperty] if data.enum is not None: - prop, schemas = build_enum_property(data=data, name=name, required=True, schemas=schemas, enum=data.enum) + prop, schemas = build_enum_property( + data=data, name=name, required=True, schemas=schemas, enum=data.enum, parent_name=None + ) else: - prop, schemas = build_model_property(data=data, name=name, schemas=schemas, required=True) + prop, schemas = build_model_property(data=data, name=name, schemas=schemas, required=True, parent_name=None) if isinstance(prop, PropertyError): return prop else: diff --git a/openapi_python_client/parser/responses.py b/openapi_python_client/parser/responses.py index a7c9a5e74..93071dff3 100644 --- a/openapi_python_client/parser/responses.py +++ b/openapi_python_client/parser/responses.py @@ -32,7 +32,7 @@ def empty_response(status_code: int, response_name: str) -> Response: def response_from_data( - *, status_code: int, data: Union[oai.Response, oai.Reference], schemas: Schemas + *, status_code: int, data: Union[oai.Response, oai.Reference], schemas: Schemas, parent_name: str ) -> Tuple[Union[Response, ParseError], Schemas]: """ Generate a Response from the OpenAPI dictionary representation of it """ @@ -58,7 +58,13 @@ def response_from_data( schemas, ) - prop, schemas = property_from_data(name=response_name, required=True, data=schema_data, schemas=schemas) + prop, schemas = property_from_data( + name=response_name, + required=True, + data=schema_data, + schemas=schemas, + parent_name=f"{parent_name}", + ) if isinstance(prop, PropertyError): return prop, schemas diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index 6df75bc70..e3caafa9e 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -126,9 +126,11 @@ def test_parse_request_json_body(self, mocker): property_from_data = mocker.patch(f"{MODULE_NAME}.property_from_data") schemas = Schemas() - result = Endpoint.parse_request_json_body(body=body, schemas=schemas) + result = Endpoint.parse_request_json_body(body=body, schemas=schemas, parent_name="parent") - property_from_data.assert_called_once_with("json_body", required=True, data=schema, schemas=schemas) + property_from_data.assert_called_once_with( + name="json_body", required=True, data=schema, schemas=schemas, parent_name="parent" + ) assert result == property_from_data.return_value def test_parse_request_json_body_no_data(self): @@ -137,7 +139,7 @@ def test_parse_request_json_body_no_data(self): body = oai.RequestBody.construct(content={}) schemas = Schemas() - result = Endpoint.parse_request_json_body(body=body, schemas=schemas) + result = Endpoint.parse_request_json_body(body=body, schemas=schemas, parent_name="parent") assert result == (None, schemas) @@ -230,7 +232,7 @@ def test_add_body_happy(self, mocker): assert response_schemas == parsed_schemas parse_request_form_body.assert_called_once_with(request_body) - parse_request_json_body.assert_called_once_with(body=request_body, schemas=initial_schemas) + parse_request_json_body.assert_called_once_with(body=request_body, schemas=initial_schemas, parent_name="name") parse_multipart_body.assert_called_once_with(request_body) import_string_from_reference.assert_has_calls( [ @@ -270,8 +272,8 @@ def test__add_responses_error(self, mocker): response_from_data.assert_has_calls( [ - mocker.call(status_code=200, data=response_1_data, schemas=schemas), - mocker.call(status_code=404, data=response_2_data, schemas=schemas), + mocker.call(status_code=200, data=response_1_data, schemas=schemas, parent_name="name"), + mocker.call(status_code=404, data=response_2_data, schemas=schemas, parent_name="name"), ] ) assert response.errors == [ @@ -325,8 +327,8 @@ def test__add_responses(self, mocker): response_from_data.assert_has_calls( [ - mocker.call(status_code=200, data=response_1_data, schemas=schemas), - mocker.call(status_code=404, data=response_2_data, schemas=schemas_1), + mocker.call(status_code=200, data=response_1_data, schemas=schemas, parent_name="name"), + mocker.call(status_code=404, data=response_2_data, schemas=schemas_1, parent_name="name"), ] ) assert endpoint.responses == [response_1, response_2] @@ -455,9 +457,15 @@ def test__add_parameters_happy(self, mocker): property_from_data.assert_has_calls( [ - mocker.call(name="path_prop_name", required=True, data=path_schema, schemas=initial_schemas), - mocker.call(name="query_prop_name", required=False, data=query_schema, schemas=schemas_1), - mocker.call(name="header_prop_name", required=False, data=header_schema, schemas=schemas_2), + mocker.call( + name="path_prop_name", required=True, data=path_schema, schemas=initial_schemas, parent_name="name" + ), + mocker.call( + name="query_prop_name", required=False, data=query_schema, schemas=schemas_1, parent_name="name" + ), + mocker.call( + name="header_prop_name", required=False, data=header_schema, schemas=schemas_2, parent_name="name" + ), ] ) path_prop.get_imports.assert_called_once_with(prefix="...") diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 03731f143..c323fb57a 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -484,21 +484,23 @@ def test_property_from_data_str_enum(self, mocker): schemas = Schemas(enums={"AnEnum": mocker.MagicMock()}) - prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) + prop, new_schemas = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent" + ) assert prop == EnumProperty( name="my_enum", required=True, nullable=False, values={"A": "A", "B": "B", "C": "C"}, - reference=Reference(class_name="AnEnum1", module_name="an_enum1"), + reference=Reference(class_name="ParentAnEnum", module_name="parent_an_enum"), value_type=str, - default="AnEnum1.B", + default="ParentAnEnum.B", ) assert schemas != new_schemas, "Provided Schemas was mutated" assert new_schemas.enums == { "AnEnum": schemas.enums["AnEnum"], - "AnEnum1": prop, + "ParentAnEnum": prop, } def test_property_from_data_int_enum(self, mocker): @@ -513,21 +515,23 @@ def test_property_from_data_int_enum(self, mocker): schemas = Schemas(enums={"AnEnum": mocker.MagicMock()}) - prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) + prop, new_schemas = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent" + ) assert prop == EnumProperty( name="my_enum", required=True, nullable=False, values={"VALUE_1": 1, "VALUE_2": 2, "VALUE_3": 3}, - reference=Reference(class_name="AnEnum1", module_name="an_enum1"), + reference=Reference(class_name="ParentAnEnum", module_name="parent_an_enum"), value_type=int, - default="AnEnum1.VALUE_3", + default="ParentAnEnum.VALUE_3", ) assert schemas != new_schemas, "Provided Schemas was mutated" assert new_schemas.enums == { "AnEnum": schemas.enums["AnEnum"], - "AnEnum1": prop, + "ParentAnEnum": prop, } def test_property_from_data_ref_enum(self): @@ -546,7 +550,7 @@ def test_property_from_data_ref_enum(self): ) schemas = Schemas(enums={"MyEnum": existing_enum}) - prop, new_schemas = property_from_data(name=name, required=False, data=data, schemas=schemas) + prop, new_schemas = property_from_data(name=name, required=False, data=data, schemas=schemas, parent_name="") assert prop == EnumProperty( name="some_enum", @@ -579,7 +583,7 @@ def test_property_from_data_ref_model(self): ) schemas = Schemas(models={class_name: existing_model}) - prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) + prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="") assert prop == ModelProperty( name=name, @@ -604,7 +608,9 @@ def test_property_from_data_ref_not_found(self, mocker): mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) schemas = Schemas() - prop, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) + prop, new_schemas = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent" + ) from_ref.assert_called_once_with(data.ref) assert prop == PropertyError(data=data, detail="Could not find reference in parsed models or enums") @@ -620,7 +626,9 @@ def test_property_from_data_string(self, mocker): mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) schemas = Schemas() - p, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) + p, new_schemas = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent" + ) assert p == _string_based_property.return_value assert schemas == new_schemas @@ -642,7 +650,9 @@ def test_property_from_data_simple_types(self, openapi_type, prop_type, python_t data = oai.Schema.construct(type=openapi_type, default=1) schemas = Schemas() - p, new_schemas = property_from_data(name=name, required=required, data=data, schemas=schemas) + p, new_schemas = property_from_data( + name=name, required=required, data=data, schemas=schemas, parent_name="parent" + ) assert p == prop_type(name=name, required=required, default=python_type(data.default), nullable=False) assert new_schemas == schemas @@ -651,22 +661,12 @@ def test_property_from_data_simple_types(self, openapi_type, prop_type, python_t data.default = 0 data.nullable = True - p, _ = property_from_data( - name=name, - required=required, - data=data, - schemas=schemas, - ) + p, _ = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="parent") assert p == prop_type(name=name, required=required, default=python_type(data.default), nullable=True) # Test bad default value data.default = "a" - p, _ = property_from_data( - name=name, - required=required, - data=data, - schemas=schemas, - ) + p, _ = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="parent") assert python_type is bool or isinstance(p, PropertyError) def test_property_from_data_array(self, mocker): @@ -682,10 +682,12 @@ def test_property_from_data_array(self, mocker): mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) schemas = Schemas() - response = property_from_data(name=name, required=required, data=data, schemas=schemas) + response = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="parent") assert response == build_list_property.return_value - build_list_property.assert_called_once_with(data=data, name=name, required=required, schemas=schemas) + build_list_property.assert_called_once_with( + data=data, name=name, required=required, schemas=schemas, parent_name="parent" + ) def test_property_from_data_union(self, mocker): from openapi_python_client.parser.properties import Schemas, property_from_data @@ -702,10 +704,12 @@ def test_property_from_data_union(self, mocker): mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) schemas = Schemas() - response = property_from_data(name=name, required=required, data=data, schemas=schemas) + response = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="parent") assert response == build_union_property.return_value - build_union_property.assert_called_once_with(data=data, name=name, required=required, schemas=schemas) + build_union_property.assert_called_once_with( + data=data, name=name, required=required, schemas=schemas, parent_name="parent" + ) def test_property_from_data_unsupported_type(self, mocker): name = mocker.MagicMock() @@ -715,7 +719,7 @@ def test_property_from_data_unsupported_type(self, mocker): from openapi_python_client.parser.errors import PropertyError from openapi_python_client.parser.properties import Schemas, property_from_data - assert property_from_data(name=name, required=required, data=data, schemas=Schemas()) == ( + assert property_from_data(name=name, required=required, data=data, schemas=Schemas(), parent_name="parent") == ( PropertyError(data=data, detail=f"unknown type {data.type}"), Schemas(), ) @@ -726,7 +730,9 @@ def test_property_from_data_no_valid_props_in_data(self): schemas = Schemas() data = oai.Schema() - prop, new_schemas = property_from_data(name="blah", required=True, data=data, schemas=schemas) + prop, new_schemas = property_from_data( + name="blah", required=True, data=data, schemas=schemas, parent_name="parent" + ) assert prop == NoneProperty(name="blah", required=True, nullable=False, default=None) assert new_schemas == schemas @@ -739,7 +745,9 @@ def test_property_from_data_validation_error(self, mocker): schemas = Schemas() data = oai.Schema() - err, new_schemas = property_from_data(name="blah", required=True, data=data, schemas=schemas) + err, new_schemas = property_from_data( + name="blah", required=True, data=data, schemas=schemas, parent_name="parent" + ) assert err == PropertyError(detail="Failed to validate default value", data=data) assert new_schemas == schemas @@ -754,7 +762,9 @@ def test_build_list_property_no_items(self, mocker): property_from_data = mocker.patch.object(properties, "property_from_data") schemas = properties.Schemas() - p, new_schemas = properties.build_list_property(name=name, required=required, data=data, schemas=schemas) + p, new_schemas = properties.build_list_property( + name=name, required=required, data=data, schemas=schemas, parent_name="parent" + ) assert p == PropertyError(data=data, detail="type array must have items defined") assert new_schemas == schemas @@ -763,7 +773,7 @@ def test_build_list_property_no_items(self, mocker): def test_build_list_property_invalid_items(self, mocker): from openapi_python_client.parser import properties - name = mocker.MagicMock() + name = "name" required = mocker.MagicMock() data = oai.Schema( type="array", @@ -775,17 +785,21 @@ def test_build_list_property_invalid_items(self, mocker): properties, "property_from_data", return_value=(properties.PropertyError(data="blah"), second_schemas) ) - p, new_schemas = properties.build_list_property(name=name, required=required, data=data, schemas=schemas) + p, new_schemas = properties.build_list_property( + name=name, required=required, data=data, schemas=schemas, parent_name="parent" + ) assert p == PropertyError(data="blah", detail=f"invalid data in items of array {name}") assert new_schemas == second_schemas assert schemas != new_schemas, "Schema was mutated" - property_from_data.assert_called_once_with(name=f"{name}_item", required=True, data=data.items, schemas=schemas) + property_from_data.assert_called_once_with( + name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name="parent_name" + ) def test_build_list_property(self, mocker): from openapi_python_client.parser import properties - name = mocker.MagicMock() + name = "prop" required = mocker.MagicMock() data = oai.Schema( type="array", @@ -799,13 +813,17 @@ def test_build_list_property(self, mocker): mocker.patch("openapi_python_client.utils.snake_case", return_value=name) mocker.patch("openapi_python_client.utils.to_valid_python_identifier", return_value=name) - p, new_schemas = properties.build_list_property(name=name, required=required, data=data, schemas=schemas) + p, new_schemas = properties.build_list_property( + name=name, required=required, data=data, schemas=schemas, parent_name="parent" + ) assert isinstance(p, properties.ListProperty) assert p.inner_property == property_from_data.return_value[0] assert new_schemas == second_schemas assert schemas != new_schemas, "Schema was mutated" - property_from_data.assert_called_once_with(name=f"{name}_item", required=True, data=data.items, schemas=schemas) + property_from_data.assert_called_once_with( + name=f"{name}_item", required=True, data=data.items, schemas=schemas, parent_name="parent_prop" + ) class TestBuildUnionProperty: @@ -825,7 +843,7 @@ def test_property_from_data_union(self, mocker): from openapi_python_client.parser.properties import Schemas, property_from_data - p, s = property_from_data(name=name, required=required, data=data, schemas=Schemas()) + p, s = property_from_data(name=name, required=required, data=data, schemas=Schemas(), parent_name="parent") FloatProperty.assert_called_once_with(name=name, required=required, default=0.0, nullable=False) IntProperty.assert_called_once_with(name=name, required=required, default=0, nullable=False) @@ -847,7 +865,7 @@ def test_property_from_data_union_bad_type(self, mocker): from openapi_python_client.parser.properties import Schemas, property_from_data - p, s = property_from_data(name=name, required=required, data=data, schemas=Schemas()) + p, s = property_from_data(name=name, required=required, data=data, schemas=Schemas(), parent_name="parent") assert p == PropertyError(detail=f"Invalid property in union {name}", data=oai.Schema(type="garbage")) @@ -960,10 +978,10 @@ def test_build_schemas(mocker): build_model_property.assert_has_calls( [ - mocker.call(data=in_data["1"], name="1", schemas=Schemas(), required=True), - mocker.call(data=in_data["2"], name="2", schemas=schemas_1, required=True), - mocker.call(data=in_data["3"], name="3", schemas=schemas_2, required=True), - mocker.call(data=in_data["3"], name="3", schemas=schemas_2, required=True), + mocker.call(data=in_data["1"], name="1", schemas=Schemas(), required=True, parent_name=None), + mocker.call(data=in_data["2"], name="2", schemas=schemas_1, required=True, parent_name=None), + mocker.call(data=in_data["3"], name="3", schemas=schemas_2, required=True, parent_name=None), + mocker.call(data=in_data["3"], name="3", schemas=schemas_2, required=True, parent_name=None), ] ) # schemas_3 was the last to come back from build_model_property, but it should be ignored because it's an error From f17f9d0f652b8b8856444ede29265853171939c2 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 8 Nov 2020 09:28:34 -0700 Subject: [PATCH 23/25] Unit test coverage for new property/response code --- .../parser/properties/model_property.py | 3 +- openapi_python_client/parser/responses.py | 4 +- .../{ref_property.pyi => model_property.pyi} | 0 .../test_properties/test_converter.py | 41 ++++++ .../test_parser/test_properties/test_init.py | 134 +++++++++++++++++- .../test_properties/test_model_property.py | 57 ++++++++ tests/test_parser/test_responses.py | 77 +++++++++- 7 files changed, 311 insertions(+), 5 deletions(-) rename openapi_python_client/templates/property_templates/{ref_property.pyi => model_property.pyi} (100%) create mode 100644 tests/test_parser/test_properties/test_converter.py create mode 100644 tests/test_parser/test_properties/test_model_property.py diff --git a/openapi_python_client/parser/properties/model_property.py b/openapi_python_client/parser/properties/model_property.py index 34becd00b..ca36171af 100644 --- a/openapi_python_client/parser/properties/model_property.py +++ b/openapi_python_client/parser/properties/model_property.py @@ -17,8 +17,7 @@ class ModelProperty(Property): description: str relative_imports: Set[str] - template: ClassVar[str] = "ref_property.pyi" - # TODO: change to model_property.pyi + template: ClassVar[str] = "model_property.pyi" def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ diff --git a/openapi_python_client/parser/responses.py b/openapi_python_client/parser/responses.py index 93071dff3..c6c6a49a1 100644 --- a/openapi_python_client/parser/responses.py +++ b/openapi_python_client/parser/responses.py @@ -1,3 +1,5 @@ +__all__ = ["Response", "response_from_data"] + from typing import Tuple, Union import attr @@ -63,7 +65,7 @@ def response_from_data( required=True, data=schema_data, schemas=schemas, - parent_name=f"{parent_name}", + parent_name=parent_name, ) if isinstance(prop, PropertyError): diff --git a/openapi_python_client/templates/property_templates/ref_property.pyi b/openapi_python_client/templates/property_templates/model_property.pyi similarity index 100% rename from openapi_python_client/templates/property_templates/ref_property.pyi rename to openapi_python_client/templates/property_templates/model_property.pyi diff --git a/tests/test_parser/test_properties/test_converter.py b/tests/test_parser/test_properties/test_converter.py new file mode 100644 index 000000000..07ca1cbf3 --- /dev/null +++ b/tests/test_parser/test_properties/test_converter.py @@ -0,0 +1,41 @@ +import pytest + +from openapi_python_client.parser.errors import ValidationError +from openapi_python_client.parser.properties.converter import convert, convert_chain + + +def test_convert_none(): + assert convert("blah", None) is None + + +def test_convert_bad_type(): + with pytest.raises(ValidationError): + assert convert("blah", "blah") + + +def test_convert_exception(): + with pytest.raises(ValidationError): + assert convert("datetime.datetime", "blah") + + +def test_convert_str(): + # This looks ugly, but it outputs in jinja as '\\"str\\"' + # The extra escape of " is not necessary but the code is overly cautious + assert convert("str", '"str"') == "'\\\\\"str\\\\\"'" + + +def test_convert_datetime(): + assert convert("datetime.datetime", "2021-01-20") == "isoparse('2021-01-20')" + + +def test_convert_date(): + assert convert("datetime.date", "2021-01-20") == "isoparse('2021-01-20').date()" + + +def test_convert_chain_no_valid(): + with pytest.raises(ValidationError): + convert_chain(("int",), "a") + + +def test_convert_chain(): + assert convert_chain(("int", "bool"), "a") diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index c323fb57a..80bd74858 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -2,7 +2,15 @@ import openapi_python_client.schema as oai from openapi_python_client.parser.errors import PropertyError, ValidationError -from openapi_python_client.parser.properties import BooleanProperty, FloatProperty, IntProperty +from openapi_python_client.parser.properties import ( + BooleanProperty, + DateTimeProperty, + FloatProperty, + IntProperty, + ModelProperty, + StringProperty, +) +from openapi_python_client.parser.reference import Reference MODULE_NAME = "openapi_python_client.parser.properties" @@ -689,6 +697,25 @@ def test_property_from_data_array(self, mocker): data=data, name=name, required=required, schemas=schemas, parent_name="parent" ) + def test_property_from_data_object(self, mocker): + from openapi_python_client.parser.properties import Schemas, property_from_data + + name = mocker.MagicMock() + required = mocker.MagicMock() + data = oai.Schema( + type="object", + ) + build_model_property = mocker.patch(f"{MODULE_NAME}.build_model_property") + mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) + schemas = Schemas() + + response = property_from_data(name=name, required=required, data=data, schemas=schemas, parent_name="parent") + + assert response == build_model_property.return_value + build_model_property.assert_called_once_with( + data=data, name=name, required=required, schemas=schemas, parent_name="parent" + ) + def test_property_from_data_union(self, mocker): from openapi_python_client.parser.properties import Schemas, property_from_data @@ -1010,3 +1037,108 @@ def test_build_enums(mocker): build_enum_property.assert_called() build_model_property.assert_not_called() + + +def test_build_model_property(): + from openapi_python_client.parser.properties import Schemas, build_model_property + + data = oai.Schema.construct( + required=["req"], + title="MyModel", + properties={ + "req": oai.Schema.construct(type="string"), + "opt": oai.Schema(type="string", format="date-time"), + }, + description="A class called MyModel", + nullable=False, + ) + schemas = Schemas(models={"OtherModel": None}) + + model, new_schemas = build_model_property( + data=data, + name="prop", + schemas=schemas, + required=True, + parent_name="parent", + ) + + assert new_schemas != schemas + assert new_schemas.models == { + "OtherModel": None, + "ParentMyModel": model, + } + assert model == ModelProperty( + name="prop", + required=True, + nullable=False, + default=None, + reference=Reference(class_name="ParentMyModel", module_name="parent_my_model"), + required_properties=[StringProperty(name="req", required=True, nullable=False, default=None)], + optional_properties=[DateTimeProperty(name="opt", required=True, nullable=False, default=None)], + description=data.description, + relative_imports={"from dateutil.parser import isoparse", "from typing import cast", "import datetime"}, + ) + + +def test_build_model_property_bad_prop(): + from openapi_python_client.parser.properties import Schemas, build_model_property + + data = oai.Schema( + properties={ + "bad": oai.Schema(type="not_real"), + }, + ) + schemas = Schemas(models={"OtherModel": None}) + + err, new_schemas = build_model_property( + data=data, + name="prop", + schemas=schemas, + required=True, + parent_name=None, + ) + + assert new_schemas == schemas + assert err == PropertyError(detail="unknown type not_real", data=oai.Schema(type="not_real")) + + +def test_build_enum_property_conflict(mocker): + from openapi_python_client.parser.properties import Schemas, build_enum_property + + data = oai.Schema() + schemas = Schemas(enums={"Existing": mocker.MagicMock()}) + + err, schemas = build_enum_property( + data=data, name="Existing", required=True, schemas=schemas, enum=[], parent_name=None + ) + + assert schemas == schemas + assert err == PropertyError(detail="Found conflicting enums named Existing with incompatible values.", data=data) + + +def test_build_enum_property_no_values(): + from openapi_python_client.parser.properties import Schemas, build_enum_property + + data = oai.Schema() + schemas = Schemas() + + err, schemas = build_enum_property( + data=data, name="Existing", required=True, schemas=schemas, enum=[], parent_name=None + ) + + assert schemas == schemas + assert err == PropertyError(detail="No values provided for Enum", data=data) + + +def test_build_enum_property_bad_default(): + from openapi_python_client.parser.properties import Schemas, build_enum_property + + data = oai.Schema(default="B") + schemas = Schemas() + + err, schemas = build_enum_property( + data=data, name="Existing", required=True, schemas=schemas, enum=["A"], parent_name=None + ) + + assert schemas == schemas + assert err == PropertyError(detail="B is an invalid default for enum Existing", data=data) diff --git a/tests/test_parser/test_properties/test_model_property.py b/tests/test_parser/test_properties/test_model_property.py new file mode 100644 index 000000000..72c8f27f1 --- /dev/null +++ b/tests/test_parser/test_properties/test_model_property.py @@ -0,0 +1,57 @@ +import pytest + + +@pytest.mark.parametrize( + "no_optional,nullable,required,expected", + [ + (False, False, False, "Union[MyClass, Unset]"), + (False, False, True, "MyClass"), + (False, True, False, "Union[Optional[MyClass], Unset]"), + (False, True, True, "Optional[MyClass]"), + (True, False, False, "MyClass"), + (True, False, True, "MyClass"), + (True, True, False, "MyClass"), + (True, True, True, "MyClass"), + ], +) +def test_get_type_string(no_optional, nullable, required, expected): + from openapi_python_client.parser.properties import ModelProperty, Reference + + prop = ModelProperty( + name="prop", + required=required, + nullable=nullable, + default=None, + reference=Reference(class_name="MyClass", module_name="my_module"), + description="", + optional_properties=[], + required_properties=[], + relative_imports=set(), + ) + + assert prop.get_type_string(no_optional=no_optional) == expected + + +def test_get_imports(): + from openapi_python_client.parser.properties import ModelProperty, Reference + + prop = ModelProperty( + name="prop", + required=False, + nullable=True, + default=None, + reference=Reference(class_name="MyClass", module_name="my_module"), + description="", + optional_properties=[], + required_properties=[], + relative_imports=set(), + ) + + assert prop.get_imports(prefix="..") == { + "from typing import Optional", + "from typing import Union", + "from ..types import UNSET, Unset", + "from ..models.my_module import MyClass", + "from typing import Dict", + "from typing import cast", + } diff --git a/tests/test_parser/test_responses.py b/tests/test_parser/test_responses.py index 20e5adc7b..eb20fb338 100644 --- a/tests/test_parser/test_responses.py +++ b/tests/test_parser/test_responses.py @@ -1,5 +1,80 @@ import openapi_python_client.schema as oai +from openapi_python_client.parser.errors import ParseError, PropertyError +from openapi_python_client.parser.properties import NoneProperty, Schemas, StringProperty MODULE_NAME = "openapi_python_client.parser.responses" -# TODO: Test response_from_data + +def test_response_from_data_no_content(): + from openapi_python_client.parser.responses import Response, response_from_data + + response, schemas = response_from_data( + status_code=200, data=oai.Response.construct(description=""), schemas=Schemas(), parent_name="parent" + ) + + assert response == Response( + status_code=200, + prop=NoneProperty(name="response_200", default=None, nullable=False, required=True), + source="None", + ) + + +def test_response_from_data_unsupported_content_type(): + from openapi_python_client.parser.responses import response_from_data + + data = oai.Response.construct(description="", content={"blah": None}) + response, schemas = response_from_data(status_code=200, data=data, schemas=Schemas(), parent_name="parent") + + assert response == ParseError(data=data, detail="Unsupported content_type {'blah': None}") + + +def test_response_from_data_no_content_schema(): + from openapi_python_client.parser.responses import Response, response_from_data + + data = oai.Response.construct(description="", content={"application/json": oai.MediaType.construct()}) + response, schemas = response_from_data(status_code=200, data=data, schemas=Schemas(), parent_name="parent") + + assert response == Response( + status_code=200, + prop=NoneProperty(name="response_200", default=None, nullable=False, required=True), + source="None", + ) + + +def test_response_from_data_property_error(mocker): + from openapi_python_client.parser import responses + + property_from_data = mocker.patch.object(responses, "property_from_data", return_value=(PropertyError(), Schemas())) + data = oai.Response.construct( + description="", content={"application/json": oai.MediaType.construct(media_type_schema="something")} + ) + response, schemas = responses.response_from_data( + status_code=400, data=data, schemas=Schemas(), parent_name="parent" + ) + + assert response == PropertyError() + property_from_data.assert_called_once_with( + name="response_400", required=True, data="something", schemas=Schemas(), parent_name="parent" + ) + + +def test_response_from_data_property(mocker): + from openapi_python_client.parser import responses + + prop = StringProperty(name="prop", required=True, nullable=False, default=None) + property_from_data = mocker.patch.object(responses, "property_from_data", return_value=(prop, Schemas())) + data = oai.Response.construct( + description="", content={"application/json": oai.MediaType.construct(media_type_schema="something")} + ) + response, schemas = responses.response_from_data( + status_code=400, data=data, schemas=Schemas(), parent_name="parent" + ) + + assert response == responses.Response( + status_code=400, + prop=prop, + source="response.json()", + ) + property_from_data.assert_called_once_with( + name="response_400", required=True, data="something", schemas=Schemas(), parent_name="parent" + ) From 45da271b9a56f4b65ad598d0dd29439d6b12b24e Mon Sep 17 00:00:00 2001 From: Dylan Anthony <43723790+dbanty@users.noreply.github.com> Date: Sun, 8 Nov 2020 09:37:56 -0700 Subject: [PATCH 24/25] Fix duplicate entry in CHANGELOG --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d52a0ae9..c1289c3d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update minimum Pydantic version to support Python 3.9 ### Additions -- Better compatibility for "required" (whether or not the field must be included) and "nullable" (whether or not the field can be null) (#205 & #208). Thanks @bowenwr & @emannguitar! - Allow specifying the generated client's version using `package_version_override` in a config file. (#225 - Thanks @fyhertz!) From c01a89f65f83d1b760bd6902b1220d9c70022f7b Mon Sep 17 00:00:00 2001 From: Dylan Anthony <43723790+dbanty@users.noreply.github.com> Date: Mon, 9 Nov 2020 10:40:51 -0700 Subject: [PATCH 25/25] Add note in CHANGELOG about `File` response --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1289c3d0..bc9c09d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 The previous behavior was a combination of skipping and using generic Dicts for these schemas. - Response schema handling was unified with input schema handling, meaning that responses will behave differently than before. Specifically, instead of the content-type deciding what the generated Python type is, the schema itself will. + - As a result of this, endpoints that used to return `bytes` when content-type was application/octet-stream will now return a `File` object if the type of the data is "binary", just like if you were submitting that type instead of receiving it. - Instead of skipping input properties with no type, enum, anyOf, or oneOf declared, the property will be declared as `None`. - Class (models and Enums) names will now contain the name of their parent element (if any). For example, a property declared in an endpoint will be named like {endpoint_name}_{previous_class_name}. Classes will no longer be