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/CHANGELOG.md b/CHANGELOG.md index 07ba58b7f..bc9c09d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,22 @@ 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. + - 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 + 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 - 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/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/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/default/__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/default/__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 87% 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 index ed054315d..18772a156 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/custom_e2e/api/tests/defaults_tests_defaults_post.py @@ -2,10 +2,12 @@ import httpx +from ...types import Response + Client = httpx.Client import datetime -from typing import Dict, List, Optional, Union, cast +from typing import Dict, List, Union from dateutil.parser import isoparse @@ -16,14 +18,18 @@ 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 -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, @@ -34,7 +40,6 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[Union[None, H def httpx_request( *, client: Client, - json_body: Dict[Any, Any], 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(), @@ -44,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): @@ -94,12 +99,9 @@ def httpx_request( if enum_prop is not UNSET: params["enum_prop"] = json_enum_prop - json_json_body = json_body - 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/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 67% 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 index 20d7c43ab..e048fb001 100644 --- 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 @@ -2,17 +2,23 @@ import httpx +from ...types import Response + 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 -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, @@ -23,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/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 66% 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 index e3fc2e4d4..a4e9f5fd4 100644 --- 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 @@ -2,17 +2,23 @@ import httpx +from ...types import Response + 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 -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, @@ -23,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/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 67% 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 index 28ec4963c..232a3c7bb 100644 --- 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 @@ -2,17 +2,23 @@ import httpx +from ...types import Response + 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 -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, @@ -23,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/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 67% 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 index 1acdf6a40..a16a16932 100644 --- 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 @@ -2,17 +2,23 @@ import httpx +from ...types import Response + 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 -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, @@ -23,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/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 69% 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 index d3a8591bc..2821664fb 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/custom_e2e/api/tests/get_user_list.py @@ -2,10 +2,12 @@ import httpx +from ...types import Response + Client = httpx.Client import datetime -from typing import Dict, List, Union, cast +from typing import Dict, List, Union from ...models.a_model import AModel from ...models.an_enum import AnEnum @@ -14,14 +16,22 @@ 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 -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, @@ -34,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/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 69% 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 index 068e6e9c7..855a8d999 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/custom_e2e/api/tests/int_enum_tests_int_enum_post.py @@ -2,9 +2,11 @@ import httpx +from ...types import Response + Client = httpx.Client -from typing import Dict, cast +from typing import Dict from ...models.an_int_enum import AnIntEnum from ...models.http_validation_error import HTTPValidationError @@ -12,14 +14,18 @@ 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 -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, @@ -31,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/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 69% 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 index 76f4ffe84..30f80e33d 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/custom_e2e/api/tests/json_body_tests_json_body_post.py @@ -2,9 +2,9 @@ import httpx -Client = httpx.Client +from ...types import Response -from typing import Dict, cast +Client = httpx.Client from ...models.a_model import AModel from ...models.http_validation_error import HTTPValidationError @@ -12,14 +12,18 @@ 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 -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, @@ -31,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/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 71% 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 index 4f4d89cbd..09707caf9 100644 --- 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 @@ -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/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 57% 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 index 8f1b83adb..409dc8a0c 100644 --- 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 @@ -2,17 +2,25 @@ import httpx +from ...types import Response + Client = httpx.Client +from io import BytesIO + +from ...types import File -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) -> httpx.Response[bytes]: - return httpx.Response( +def _build_response(*, response: httpx.Response) -> Response[File]: + return Response( status_code=response.status_code, content=response.content, headers=response.headers, @@ -23,7 +31,7 @@ def _build_response(*, response: httpx.Response) -> httpx.Response[bytes]: def httpx_request( *, client: Client, -) -> httpx.Response[bytes]: +) -> Response[File]: response = client.request( "get", 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 72% 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 index bce044ca0..bff43cc10 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/custom_e2e/api/tests/optional_value_tests_optional_query_param.py @@ -2,9 +2,11 @@ import httpx +from ...types import Response + Client = httpx.Client -from typing import List, Optional, Union +from typing import Dict, List, Union from ...models.http_validation_error import HTTPValidationError from ...types import UNSET, Unset @@ -12,14 +14,18 @@ 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 -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, @@ -31,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 new file mode 100644 index 000000000..8dcede9cb --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/api/tests/test_inline_objects.py @@ -0,0 +1,44 @@ +from typing import Optional + +import httpx + +from ...types import Response + +Client = httpx.Client + +from ...models.test_inline_objectsjson_body import TestInlineObjectsjsonBody +from ...models.test_inline_objectsresponse_200 import TestInlineObjectsresponse_200 + + +def _parse_response(*, response: httpx.Response) -> Optional[TestInlineObjectsresponse_200]: + if response.status_code == 200: + response_200 = TestInlineObjectsresponse_200.from_dict(response.json()) + + return response_200 + return None + + +def _build_response(*, response: httpx.Response) -> Response[TestInlineObjectsresponse_200]: + return Response( + status_code=response.status_code, + content=response.content, + headers=response.headers, + parsed=_parse_response(response=response), + ) + + +def httpx_request( + *, + client: Client, + json_body: TestInlineObjectsjsonBody, +) -> Response[TestInlineObjectsresponse_200]: + + json_json_body = json_body.to_dict() + + response = client.request( + "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/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 71% 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 index c1019e884..333374cdb 100644 --- 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 @@ -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/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 76% 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 index e294e6fae..2d084a9e0 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/custom_e2e/api/tests/upload_file_tests_upload_post.py @@ -2,9 +2,11 @@ import httpx +from ...types import Response + Client = httpx.Client -from typing import Optional, 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,18 +18,22 @@ 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 -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, @@ -39,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/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 72% 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 index 5cf14bb24..5ae4c16b7 100644 --- 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 @@ -6,4 +6,6 @@ from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from .different_enum import DifferentEnum from .http_validation_error import HTTPValidationError +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/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/a_model.py similarity index 62% 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 index a2fd94276..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/custom_e2e/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,12 +16,11 @@ 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]] + 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 @@ -36,19 +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_item.append(nested_list_of_enums_item_item) + 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.append(nested_list_of_enums_item) + nested_list_of_enums_item.append(nested_list_of_enums_item_item) - some_dict = self.some_dict if self.some_dict else None + nested_list_of_enums.append(nested_list_of_enums_item) attr_1_leading_digit = self.attr_1_leading_digit required_nullable = self.required_nullable @@ -60,17 +54,12 @@ def to_dict(self) -> Dict[str, Any]: "aCamelDateTime": a_camel_date_time, "a_date": a_date, "required_not_nullable": required_not_nullable, - "some_dict": some_dict, + "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 @@ -97,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) @@ -106,15 +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) - some_dict = d["some_dict"] - - 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, @@ -122,7 +109,6 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d 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, 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 86% 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 index 3435bd290..93f2dd68f 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/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/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/custom_e2e/models/http_validation_error.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/http_validation_error.py new file mode 100644 index 000000000..2b83121ce --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/http_validation_error.py @@ -0,0 +1,37 @@ +from typing import Any, Dict, List + +import attr + +from ..models.validation_error import ValidationError + + +@attr.s(auto_attribs=True) +class HTTPValidationError: + """ """ + + detail: List[ValidationError] + + def to_dict(self) -> Dict[str, Any]: + detail = [] + for detail_item_data in self.detail: + detail_item = detail_item_data.to_dict() + + detail.append(detail_item) + + field_dict = { + "detail": detail, + } + + return field_dict + + @staticmethod + def from_dict(d: Dict[str, Any]) -> "HTTPValidationError": + detail = [] + for detail_item_data in d["detail"]: + 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/custom_e2e/models/test_inline_objectsjson_body.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsjson_body.py new file mode 100644 index 000000000..833d8f9a0 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsjson_body.py @@ -0,0 +1,27 @@ +from typing import Any, Dict + +import attr + + +@attr.s(auto_attribs=True) +class TestInlineObjectsjsonBody: + """ """ + + 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]) -> "TestInlineObjectsjsonBody": + a_property = d["a_property"] + + return TestInlineObjectsjsonBody( + a_property=a_property, + ) diff --git a/end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsresponse_200.py b/end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsresponse_200.py new file mode 100644 index 000000000..ebe0a4d72 --- /dev/null +++ b/end_to_end_tests/golden-record-custom/custom_e2e/models/test_inline_objectsresponse_200.py @@ -0,0 +1,27 @@ +from typing import Any, Dict + +import attr + + +@attr.s(auto_attribs=True) +class TestInlineObjectsresponse_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]) -> "TestInlineObjectsresponse_200": + a_property = d["a_property"] + + return TestInlineObjectsresponse_200( + 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/custom_e2e/models/validation_error.py similarity index 88% 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 index 77b9239ef..cadbc0d5b 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/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-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/my_test_api_client/api/default/ping_ping_get.py b/end_to_end_tests/golden-record-custom/my_test_api_client/api/default/ping_ping_get.py deleted file mode 100644 index 5e4cc9a2d..000000000 --- a/end_to_end_tests/golden-record-custom/my_test_api_client/api/default/ping_ping_get.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Optional - -import httpx - -Client = httpx.Client - - -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]: - return httpx.Response( - status_code=response.status_code, - content=response.content, - headers=response.headers, - parsed=_parse_response(response=response), - ) - - -def httpx_request( - *, - client: Client, -) -> httpx.Response[bool]: - - response = client.request( - "get", - "/ping", - ) - - return _build_response(response=response) 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/my_test_api_client/api/tests/__init__.py deleted file mode 100644 index e69de29bb..000000000 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/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/golden-record/my_test_api_client/api/default/__init__.py b/end_to_end_tests/golden-record/my_test_api_client/api/default/__init__.py deleted file mode 100644 index e69de29bb..000000000 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 0991b7f73..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 @@ -13,7 +13,6 @@ def _get_kwargs( *, client: Client, - json_body: Dict[Any, Any], 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(), @@ -76,23 +75,24 @@ def _get_kwargs( if enum_prop is not UNSET: params["enum_prop"] = json_enum_prop - json_json_body = json_body - return { "url": url, "headers": headers, "cookies": client.get_cookies(), "timeout": client.get_timeout(), - "json": json_json_body, "params": params, } 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 @@ -108,7 +108,6 @@ def _build_response(*, response: httpx.Response) -> Response[Union[None, HTTPVal def sync_detailed( *, client: Client, - json_body: Dict[Any, Any], 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(), @@ -121,7 +120,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, @@ -143,7 +141,6 @@ def sync_detailed( def sync( *, client: Client, - json_body: Dict[Any, Any], 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(), @@ -158,7 +155,6 @@ def sync( return sync_detailed( client=client, - json_body=json_body, string_prop=string_prop, datetime_prop=datetime_prop, date_prop=date_prop, @@ -174,7 +170,6 @@ def sync( async def asyncio_detailed( *, client: Client, - json_body: Dict[Any, Any], 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(), @@ -187,7 +182,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, @@ -208,7 +202,6 @@ async def asyncio_detailed( async def asyncio( *, client: Client, - json_body: Dict[Any, Any], 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(), @@ -224,7 +217,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/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/default/ping_ping_get.py b/end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py similarity index 50% rename from end_to_end_tests/golden-record/my_test_api_client/api/default/ping_ping_get.py rename to end_to_end_tests/golden-record/my_test_api_client/api/tests/test_inline_objects.py index 9ee1151e0..53fc2c021 100644 --- 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/tests/test_inline_objects.py @@ -3,32 +3,40 @@ import httpx from ...client import Client +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: TestInlineObjectsjsonBody, ) -> Dict[str, Any]: - url = "{}/ping".format(client.base_url) + url = "{}/tests/inline_objects".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[bool]: +def _parse_response(*, response: httpx.Response) -> Optional[TestInlineObjectsresponse_200]: if response.status_code == 200: - return bool(response.text) + response_200 = TestInlineObjectsresponse_200.from_dict(response.json()) + + return response_200 return None -def _build_response(*, response: httpx.Response) -> Response[bool]: +def _build_response(*, response: httpx.Response) -> Response[TestInlineObjectsresponse_200]: return Response( status_code=response.status_code, content=response.content, @@ -40,12 +48,14 @@ def _build_response(*, response: httpx.Response) -> Response[bool]: def sync_detailed( *, client: Client, -) -> Response[bool]: + json_body: TestInlineObjectsjsonBody, +) -> Response[TestInlineObjectsresponse_200]: kwargs = _get_kwargs( client=client, + json_body=json_body, ) - response = httpx.get( + response = httpx.post( **kwargs, ) @@ -55,24 +65,28 @@ def sync_detailed( def sync( *, client: Client, -) -> Optional[bool]: - """ A quick check to see if the system is running """ + json_body: TestInlineObjectsjsonBody, +) -> Optional[TestInlineObjectsresponse_200]: + """ """ return sync_detailed( client=client, + json_body=json_body, ).parsed async def asyncio_detailed( *, client: Client, -) -> Response[bool]: + json_body: TestInlineObjectsjsonBody, +) -> Response[TestInlineObjectsresponse_200]: kwargs = _get_kwargs( client=client, + json_body=json_body, ) async with httpx.AsyncClient() as _client: - response = await _client.get(**kwargs) + response = await _client.post(**kwargs) return _build_response(response=response) @@ -80,11 +94,13 @@ async def asyncio_detailed( async def asyncio( *, client: Client, -) -> Optional[bool]: - """ A quick check to see if the system is running """ + json_body: TestInlineObjectsjsonBody, +) -> Optional[TestInlineObjectsresponse_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 5cf14bb24..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,4 +6,6 @@ from .body_upload_file_tests_upload_post import BodyUploadFileTestsUploadPost from .different_enum import DifferentEnum from .http_validation_error import HTTPValidationError +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/my_test_api_client/models/a_model.py b/end_to_end_tests/golden-record/my_test_api_client/models/a_model.py index a2fd94276..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,12 +16,11 @@ 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]] + 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 @@ -36,19 +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_item.append(nested_list_of_enums_item_item) + 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.append(nested_list_of_enums_item) + nested_list_of_enums_item.append(nested_list_of_enums_item_item) - some_dict = self.some_dict if self.some_dict else None + nested_list_of_enums.append(nested_list_of_enums_item) attr_1_leading_digit = self.attr_1_leading_digit required_nullable = self.required_nullable @@ -60,17 +54,12 @@ def to_dict(self) -> Dict[str, Any]: "aCamelDateTime": a_camel_date_time, "a_date": a_date, "required_not_nullable": required_not_nullable, - "some_dict": some_dict, + "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 @@ -97,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) @@ -106,15 +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) - some_dict = d["some_dict"] - - 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, @@ -122,7 +109,6 @@ def _parse_a_camel_date_time(data: Dict[str, Any]) -> Union[datetime.datetime, d 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, 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/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/test_inline_objectsjson_body.py b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsjson_body.py new file mode 100644 index 000000000..833d8f9a0 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsjson_body.py @@ -0,0 +1,27 @@ +from typing import Any, Dict + +import attr + + +@attr.s(auto_attribs=True) +class TestInlineObjectsjsonBody: + """ """ + + 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]) -> "TestInlineObjectsjsonBody": + a_property = d["a_property"] + + return TestInlineObjectsjsonBody( + a_property=a_property, + ) diff --git a/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsresponse_200.py b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsresponse_200.py new file mode 100644 index 000000000..ebe0a4d72 --- /dev/null +++ b/end_to_end_tests/golden-record/my_test_api_client/models/test_inline_objectsresponse_200.py @@ -0,0 +1,27 @@ +from typing import Any, Dict + +import attr + + +@attr.s(auto_attribs=True) +class TestInlineObjectsresponse_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]) -> "TestInlineObjectsresponse_200": + a_property = d["a_property"] + + return TestInlineObjectsresponse_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/openapi.json b/end_to_end_tests/openapi.json index 2bde7a520..f3e1a26ae 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": [ @@ -401,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", @@ -544,6 +508,48 @@ } } }, + "/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" + } + } + } + } + } + } + } + } + }, "/tests/optional_query_param/": { "get": { "tags": [ @@ -597,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": { @@ -614,14 +620,6 @@ }, "default": [] }, - "some_dict": { - "title": "Some Dict", - "type": "object", - "additionalProperties": { - "type": "string" - }, - "nullable": true - }, "aCamelDateTime": { "title": "Acameldatetime", "anyOf": [ 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( 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..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 %} @@ -18,15 +20,21 @@ 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 %} -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, @@ -38,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) }} 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/__init__.py b/openapi_python_client/__init__.py index d7b40ea2b..e42b807f0 100644 --- a/openapi_python_client/__init__.py +++ b/openapi_python_client/__init__.py @@ -108,7 +108,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: @@ -161,7 +161,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/openapi.py b/openapi_python_client/parser/openapi.py index 1626f1541..3053ee305 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -1,16 +1,16 @@ 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 .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 +from .responses import Response, response_from_data class ParameterLocation(str, Enum): @@ -35,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] = {} @@ -48,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." @@ -60,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: @@ -114,25 +118,37 @@ 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, 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) - return None + 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 - 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, parent_name=endpoint.name + ) 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) @@ -147,13 +163,15 @@ 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": + 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, parent_name=endpoint.name + ) if isinstance(response, ParseError): endpoint.errors.append( ParseError( @@ -165,22 +183,29 @@ 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(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, + parent_name=endpoint.name, + ) 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: @@ -190,11 +215,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: @@ -211,97 +238,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 - result = Endpoint._add_responses(result, data.responses) - result = Endpoint._add_body(result, data) - - 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] + return result, schemas + result, schemas = Endpoint._add_responses(endpoint=result, data=data.responses, schemas=schemas) + result, schemas = Endpoint._add_body(endpoint=result, data=data, schemas=schemas) - @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 p.required and not p.nullable: - 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 """ - - models: Dict[str, Model] = field(default_factory=dict) - errors: List[ParseError] = field(default_factory=list) - - @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 @@ -311,7 +254,8 @@ class GeneratorData: title: str description: Optional[str] version: str - schemas: Schemas + models: Dict[str, ModelProperty] + errors: List[ParseError] endpoint_collections_by_tag: Dict[str, EndpointCollection] enums: Dict[str, EnumProperty] @@ -325,15 +269,16 @@ 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, 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/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py deleted file mode 100644 index 8fc4ac605..000000000 --- a/openapi_python_client/parser/properties.py +++ /dev/null @@ -1,599 +0,0 @@ -from dataclasses import InitVar, dataclass, field -from itertools import chain -from typing import Any, ClassVar, Dict, Generic, List, Optional, Set, Type, 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 or Unset even if the value is optional (needed for isinstance checks) - """ - type_string = self._type_string - if no_optional: - return type_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]: - """ - 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 Union, Optional", f"from {prefix}types import UNSET, Unset"} - return set() - - def to_string(self) -> str: - """ How this should be declared in a dataclass """ - if self.default is not None: - default = self.default - elif not self.required: - default = "UNSET" - else: - default = None - - if default is not None: - return f"{self.python_name}: {self.get_type_string()} = {default}" - else: - return f"{self.python_name}: {self.get_type_string()}" - - -@dataclass -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 -class DateTimeProperty(Property): - """ - A property of type datetime.datetime - """ - - _type_string: ClassVar[str] = "datetime.datetime" - template: ClassVar[str] = "datetime_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.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 -class DateProperty(Property): - """ A property of type datetime.date """ - - _type_string: ClassVar[str] = "datetime.date" - template: ClassVar[str] = "date_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.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 -class FileProperty(Property): - """ A property used for uploading files """ - - _type_string: ClassVar[str] = "File" - template: ClassVar[str] = "file_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.update({f"from {prefix}types import File"}) - return imports - - -@dataclass -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 -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 -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 -class ListProperty(Property, Generic[InnerProp]): - """ A property representing a list (array) of other properties """ - - inner_property: InnerProp - template: ClassVar[str] = "list_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 """ - type_string = f"List[{self.inner_property.get_type_string()}]" - if no_optional: - return type_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]: - """ - 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(self.inner_property.get_imports(prefix=prefix)) - imports.add("from typing import List") - return imports - - def _validate_default(self, default: Any) -> None: - return None - - -@dataclass -class UnionProperty(Property): - """ A property representing a Union (anyOf) of other properties """ - - inner_properties: List[Property] - template: ClassVar[str] = "union_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 """ - 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 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}]" - return 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. - """ - imports = super().get_imports(prefix=prefix) - for inner_prop in self.inner_properties: - imports.update(inner_prop.get_imports(prefix=prefix)) - 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() - - -_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 """ - 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[Unset, {type_string}]" - return 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. - """ - 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 """ - 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[Unset, {type_string}]" - return 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. - """ - 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 """ - - _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 -) -> Union[StringProperty, DateProperty, DateTimeProperty, FileProperty]: - """ Construct a Property from the type "string" """ - string_format = data.schema_format - if string_format == "date-time": - return DateTimeProperty( - name=name, - required=required, - default=data.default, - nullable=data.nullable, - ) - elif string_format == "date": - return DateProperty( - name=name, - required=required, - default=data.default, - nullable=data.nullable, - ) - elif string_format == "binary": - return FileProperty( - name=name, - required=required, - default=data.default, - nullable=data.nullable, - ) - else: - return StringProperty( - name=name, - default=data.default, - required=required, - pattern=data.pattern, - nullable=data.nullable, - ) - - -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( - name=name, - required=required, - values=EnumProperty.values_from_list(data.enum), - title=data.title or name, - default=data.default, - 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( - name=name, - required=required, - default=data.default, - inner_properties=sub_properties, - nullable=data.nullable, - ) - if not data.type: - return PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined.") - if data.type == "string": - return _string_based_property(name=name, required=required, data=data) - elif data.type == "number": - return FloatProperty( - name=name, - default=data.default, - required=required, - nullable=data.nullable, - ) - elif data.type == "integer": - return IntProperty( - name=name, - default=data.default, - required=required, - nullable=data.nullable, - ) - elif data.type == "boolean": - return BooleanProperty( - name=name, - required=required, - default=data.default, - nullable=data.nullable, - ) - 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, - ) - 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}") - - -def property_from_data( - name: str, required: bool, data: Union[oai.Reference, oai.Schema] -) -> Union[Property, PropertyError]: - try: - return _property_from_data(name=name, required=required, data=data) - except ValidationError: - return PropertyError(detail="Failed to validate default value", data=data) diff --git a/openapi_python_client/parser/properties/__init__.py b/openapi_python_client/parser/properties/__init__.py new file mode 100644 index 000000000..1295854fd --- /dev/null +++ b/openapi_python_client/parser/properties/__init__.py @@ -0,0 +1,513 @@ +from itertools import chain +from typing import Any, ClassVar, Dict, Generic, Iterable, List, Optional, Set, Tuple, TypeVar, Union + +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 + + +@attr.s(auto_attribs=True, frozen=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) +class StringProperty(Property): + """ A property of type str """ + + max_length: Optional[int] = None + pattern: Optional[str] = None + _type_string: ClassVar[str] = "str" + + +@attr.s(auto_attribs=True, frozen=True) +class DateTimeProperty(Property): + """ + A property of type datetime.datetime + """ + + _type_string: ClassVar[str] = "datetime.datetime" + template: ClassVar[str] = "datetime_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.update({"import datetime", "from typing import cast", "from dateutil.parser import isoparse"}) + return imports + + +@attr.s(auto_attribs=True, frozen=True) +class DateProperty(Property): + """ A property of type datetime.date """ + + _type_string: ClassVar[str] = "datetime.date" + template: ClassVar[str] = "date_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.update({"import datetime", "from typing import cast", "from dateutil.parser import isoparse"}) + return imports + + +@attr.s(auto_attribs=True, frozen=True) +class FileProperty(Property): + """ A property used for uploading files """ + + _type_string: ClassVar[str] = "File" + template: ClassVar[str] = "file_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.update({f"from {prefix}types import File", "from io import BytesIO"}) + return imports + + +@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) +class IntProperty(Property): + """ A property of type int """ + + _type_string: ClassVar[str] = "int" + + +@attr.s(auto_attribs=True, frozen=True) +class BooleanProperty(Property): + """ Property for bool """ + + _type_string: ClassVar[str] = "bool" + + +InnerProp = TypeVar("InnerProp", bound=Property) + + +@attr.s(auto_attribs=True, frozen=True) +class ListProperty(Property, Generic[InnerProp]): + """ A property representing a list (array) of other properties """ + + inner_property: InnerProp + template: ClassVar[str] = "list_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 """ + type_string = f"List[{self.inner_property.get_type_string()}]" + if no_optional: + return type_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]: + """ + 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(self.inner_property.get_imports(prefix=prefix)) + imports.add("from typing import cast, List") + return imports + + +@attr.s(auto_attribs=True, frozen=True) +class UnionProperty(Property): + """ A property representing a Union (anyOf) of other properties """ + + inner_properties: List[Property] + template: ClassVar[str] = "union_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 """ + 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 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}]" + return 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. + """ + imports = super().get_imports(prefix=prefix) + for inner_prop in self.inner_properties: + imports.update(inner_prop.get_imports(prefix=prefix)) + imports.add("from typing import Union") + return imports + + +def _string_based_property( + name: str, required: bool, data: oai.Schema +) -> Union[StringProperty, DateProperty, DateTimeProperty, FileProperty]: + """ Construct a Property from the type "string" """ + string_format = data.schema_format + if string_format == "date-time": + return DateTimeProperty( + name=name, + required=required, + default=convert("datetime.datetime", data.default), + nullable=data.nullable, + ) + elif string_format == "date": + return DateProperty( + name=name, + required=required, + default=convert("datetime.date", data.default), + nullable=data.nullable, + ) + elif string_format == "binary": + return FileProperty( + name=name, + required=required, + default=None, + nullable=data.nullable, + ) + else: + return StringProperty( + name=name, + default=convert("str", data.default), + required=required, + pattern=data.pattern, + nullable=data.nullable, + ) + + +def build_model_property( + *, 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 + + 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() + + 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, parent_name=class_name + ) + if isinstance(prop, PropertyError): + return prop, schemas + if prop_required and not prop.nullable: + 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 = 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]], + parent_name: Optional[str], +) -> Tuple[Union[EnumProperty, PropertyError], Schemas]: + """ + Create an EnumProperty from schema data. + + 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) + + if reference.class_name in schemas.enums: + existing = schemas.enums[reference.class_name] + 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) + 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, + default=default, + nullable=data.nullable, + reference=reference, + values=values, + value_type=value_type, + ) + schemas = attr.evolve(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, 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, 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) + + default = convert_chain((prop._type_string for prop in sub_properties), data.default) + return ( + UnionProperty( + name=name, + required=required, + default=default, + inner_properties=sub_properties, + nullable=data.nullable, + ), + schemas, + ) + + +def build_list_property( + *, 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, 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 ( + ListProperty( + name=name, + required=required, + default=None, + inner_property=inner_prop, + nullable=data.nullable, + ), + 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]: + """ 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) + existing = schemas.enums.get(reference.class_name) or schemas.models.get(reference.class_name) + if existing: + return ( + attr.evolve(existing, required=required, name=name), + 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, parent_name=parent_name + ) + if data.anyOf or data.oneOf: + 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 + + if data.type == "string": + return _string_based_property(name=name, required=required, data=data), schemas + elif data.type == "number": + return ( + FloatProperty( + name=name, + default=convert("float", data.default), + required=required, + nullable=data.nullable, + ), + schemas, + ) + elif data.type == "integer": + return ( + IntProperty( + name=name, + default=convert("int", data.default), + required=required, + nullable=data.nullable, + ), + schemas, + ) + elif data.type == "boolean": + return ( + BooleanProperty( + name=name, + required=required, + default=convert("bool", data.default), + nullable=data.nullable, + ), + schemas, + ) + elif data.type == "array": + 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, 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, parent_name=parent_name) + except ValidationError: + 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]: + 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, parent_name=None + ) + else: + prop, schemas = build_model_property(data=data, name=name, schemas=schemas, required=True, parent_name=None) + 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() + to_process: Iterable[Tuple[str, Union[oai.Reference, oai.Schema]]] = components.items() + processing = True + errors: List[PropertyError] = [] + + # 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/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 new file mode 100644 index 000000000..1217f23ee --- /dev/null +++ b/openapi_python_client/parser/properties/enum_property.py @@ -0,0 +1,69 @@ +__all__ = ["EnumProperty"] + +from typing import Any, ClassVar, Dict, List, Optional, Set, Type, Union + +import attr + +from ... import utils +from ..reference import Reference +from .property import Property + +ValueType = Union[str, int] + + +@attr.s(auto_attribs=True, frozen=True) +class EnumProperty(Property): + """ A property that should use an enum """ + + values: Dict[str, ValueType] + reference: Reference + value_type: Type[ValueType] + default: Optional[Any] = attr.ib() + + template: ClassVar[str] = "enum_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 """ + + 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[Unset, {type_string}]" + return 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. + """ + 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 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..ca36171af --- /dev/null +++ b/openapi_python_client/parser/properties/model_property.py @@ -0,0 +1,49 @@ +from typing import ClassVar, List, Set + +import attr + +from ..reference import Reference +from .property import Property + + +@attr.s(auto_attribs=True, frozen=True) +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] = "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 """ + 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]: + """ + 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..c7649200f --- /dev/null +++ b/openapi_python_client/parser/properties/property.py @@ -0,0 +1,81 @@ +from typing import ClassVar, Optional, Set + +import attr + +from ... import utils + + +@attr.s(auto_attribs=True, frozen=True) +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 + _type_string: ClassVar[str] = "" + default: Optional[str] = attr.ib() + python_name: str = attr.ib(init=False) + + template: ClassVar[Optional[str]] = None + + 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: + """ + Get a string representation of type that should be used when declaring this property + + Args: + no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks) + """ + type_string = self._type_string + if no_optional: + return self._type_string + if self.nullable: + type_string = f"Optional[{type_string}]" + if not self.required: + type_string = f"Union[Unset, {type_string}]" + return 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. + """ + imports = set() + 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 + + 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: + default = "UNSET" + else: + default = None + + if default is not None: + 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/parser/properties/schemas.py b/openapi_python_client/parser/properties/schemas.py new file mode 100644 index 000000000..338938673 --- /dev/null +++ b/openapi_python_client/parser/properties/schemas.py @@ -0,0 +1,18 @@ +__all__ = ["Schemas"] + +from typing import Dict, List + +import attr + +from ..errors import ParseError +from .enum_property import EnumProperty +from .model_property import ModelProperty + + +@attr.s(auto_attribs=True, frozen=True) +class Schemas: + """ Structure for containing all defined, shareable, and resuabled schemas (attr classes and Enums) """ + + 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/openapi_python_client/parser/responses.py b/openapi_python_client/parser/responses.py index 871714722..c6c6a49a1 100644 --- a/openapi_python_client/parser/responses.py +++ b/openapi_python_client/parser/responses.py @@ -1,154 +1,74 @@ -from dataclasses import InitVar, dataclass, field -from typing import Union +__all__ = ["Response", "response_from_data"] + +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) 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, parent_name: str +) -> 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, + parent_name=parent_name, + ) + + 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/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/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/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___init__.py b/tests/test___init__.py index ddc491db0..2b1f25a15 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/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py deleted file mode 100644 index 892f4ec07..000000000 --- a/tests/test_openapi_parser/test_properties.py +++ /dev/null @@ -1,1086 +0,0 @@ -import pytest - -import openapi_python_client.schema as oai -from openapi_python_client.parser.errors import PropertyError, ValidationError - -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): - from openapi_python_client.parser.properties import Property - - 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 - 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 - - name = "test" - 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 - assert p.to_string() == f"{name}: {get_type_string()} = UNSET" - - p.required = True - p.nullable = True - assert p.to_string() == f"{name}: {get_type_string()}" - - p.default = "TEST" - assert p.to_string() == f"{name}: {get_type_string()} = TEST" - - def test_get_imports(self): - from openapi_python_client.parser.properties import Property - - p = Property(name="test", required=True, default=None, nullable=False) - assert p.get_imports(prefix="") == set() - - p.required = False - 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 - - # 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): - from openapi_python_client.parser.properties import StringProperty - - p = StringProperty(name="test", required=True, default=None, nullable=False) - - base_type_string = f"str" - - assert p.get_type_string() == base_type_string - - 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__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): - from openapi_python_client.parser.properties import DateTimeProperty - - p = DateTimeProperty(name="test", required=True, default=None, nullable=False) - assert p.get_imports(prefix="...") == { - "import datetime", - "from typing import cast", - "from dateutil.parser import isoparse", - } - - p.required = False - assert p.get_imports(prefix="...") == { - "import datetime", - "from typing import cast", - "from dateutil.parser import isoparse", - "from typing import Union, 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 Union, Optional", - "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): - from openapi_python_client.parser.properties import DateProperty - - p = DateProperty(name="test", required=True, default=None, nullable=False) - assert p.get_imports(prefix="...") == { - "import datetime", - "from typing import cast", - "from dateutil.parser import isoparse", - } - - p.required = False - assert p.get_imports(prefix="...") == { - "import datetime", - "from typing import cast", - "from dateutil.parser import isoparse", - "from typing import Union, 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 Union, Optional", - "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): - from openapi_python_client.parser.properties import FileProperty - - prefix = "..." - p = FileProperty(name="test", required=True, default=None, nullable=False) - assert p.get_imports(prefix=prefix) == { - "from ...types import File", - } - - p.required = False - assert p.get_imports(prefix=prefix) == { - "from ...types import File", - "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 Union, Optional", - "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): - from openapi_python_client.parser.properties import ListProperty - - inner_property = mocker.MagicMock() - inner_type_string = mocker.MagicMock() - inner_property.get_type_string.return_value = inner_type_string - p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=False) - - base_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[{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 - - inner_property = mocker.MagicMock() - inner_import = mocker.MagicMock() - inner_property.get_imports.return_value = {inner_import} - 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 Union, Optional", - "from ...types import UNSET, Unset", - } - - p.nullable = True - assert p.get_imports(prefix=prefix) == { - inner_import, - "from typing import List", - "from typing import Union, Optional", - "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): - 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_2 = mocker.MagicMock() - inner_property_2.get_type_string.return_value = "inner_type_string_2" - p = UnionProperty( - name="test", - required=True, - default=None, - inner_properties=[inner_property_1, inner_property_2], - nullable=False, - ) - - base_type_string = f"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() == 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"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() == 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 - - inner_property_1 = mocker.MagicMock() - inner_import_1 = mocker.MagicMock() - inner_property_1.get_imports.return_value = {inner_import_1} - inner_property_2 = mocker.MagicMock() - inner_import_2 = mocker.MagicMock() - inner_property_2.get_imports.return_value = {inner_import_2} - prefix = "..." - p = UnionProperty( - name="test", - required=True, - default=None, - inner_properties=[inner_property_1, inner_property_2], - nullable=False, - ) - - assert p.get_imports(prefix=prefix) == { - inner_import_1, - 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 Union, 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 Union, Optional", - "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() - 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 - ) - - 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 - ) - == 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 - ) - 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") - 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 - ) - - base_type_string = f"MyTestEnum" - - assert p.get_type_string() == base_type_string - - 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 = {} - - 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 - ) - - assert enum_property.get_imports(prefix=prefix) == { - 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 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 Union, Optional", - "from ...types import UNSET, Unset", - } - - properties._existing_enums = {} - - def test_values_from_list(self): - from openapi_python_client.parser.properties import EnumProperty - - data = ["abc", "123", "a23", "1bc", 4, -3, "a Thing WIth spaces"] - - result = EnumProperty.values_from_list(data) - - assert result == { - "ABC": "abc", - "VALUE_1": "123", - "A23": "a23", - "VALUE_3": "1bc", - "VALUE_4": 4, - "VALUE_NEGATIVE_3": -3, - "A_THING_WITH_SPACES": "a Thing WIth spaces", - } - - def test_values_from_list_duplicate(self): - from openapi_python_client.parser.properties import EnumProperty - - data = ["abc", "123", "a23", "abc"] - - 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) - - 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 - ) - 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 - ) - - 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 - - p = RefProperty( - name="test", - required=True, - default=None, - reference=mocker.MagicMock(class_name="MyRefClass"), - nullable=False, - ) - - base_type_string = f"MyRefClass" - - assert p.get_type_string() == base_type_string - - 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") - prefix = "..." - - 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 Union, 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 Union, Optional", - "from ...types import UNSET, Unset", - } - - 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: - def test_get_imports(self, mocker): - from openapi_python_client.parser.properties import DictProperty - - prefix = "..." - p = DictProperty(name="test", required=True, default=None, nullable=False) - assert p.get_imports(prefix=prefix) == { - "from typing import Dict", - } - - p.required = False - assert p.get_imports(prefix=prefix) == { - "from typing import Dict", - "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 Union, Optional", - "from ...types import UNSET, Unset", - } - - p.default = mocker.MagicMock() - assert p.get_imports(prefix=prefix) == { - "from typing import Dict", - "from typing import cast", - "from dataclasses import field", - "from typing import Union, Optional", - "from ...types import UNSET, Unset", - } - - def test__validate_default(self): - from openapi_python_client.parser.properties import DictProperty - - p = DictProperty(name="a name", required=True, default={"key": "value"}, nullable=False) - - assert p.default is None - - -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) - - from openapi_python_client.parser.properties import property_from_data - - p = property_from_data(name=name, required=required, data=data) - - 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, - ) - assert p == EnumProperty() - - EnumProperty.reset_mock() - data.title = mocker.MagicMock() - - 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, - ) - - 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) - - from openapi_python_client.parser.properties import property_from_data - - p = property_from_data(name=name, required=required, data=data) - - 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() - - def test_property_from_data_string(self, mocker): - _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) - - from openapi_python_client.parser.properties import property_from_data - - p = property_from_data(name=name, required=required, data=data) - - assert p == _string_based_property.return_value - _string_based_property.assert_called_once_with(name=name, required=required, data=data) - - @pytest.mark.parametrize( - "openapi_type,python_type", - [ - ("number", "FloatProperty"), - ("integer", "IntProperty"), - ("boolean", "BooleanProperty"), - ("object", "DictProperty"), - ], - ) - def test_property_from_data_simple_types(self, mocker, openapi_type, python_type): - 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) - - from openapi_python_client.parser.properties import property_from_data - - p = property_from_data(name=name, required=required, data=data) - - clazz.assert_called_once_with(name=name, required=required, default=None, nullable=False) - assert p == clazz() - - # Test optional values - clazz.reset_mock() - data.default = mocker.MagicMock() - data.nullable = mocker.MagicMock() - - property_from_data( - name=name, - required=required, - data=data, - ) - clazz.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable) - - def test_property_from_data_array(self, mocker): - 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") - mocker.patch("openapi_python_client.utils.remove_string_escapes", return_value=name) - - from openapi_python_client.parser.properties import property_from_data - - p = property_from_data(name=name, required=required, data=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 - ) - assert p == ListProperty.return_value - - def test_property_from_data_array_no_items(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Schema(type="array") - - from openapi_python_client.parser.properties import property_from_data - - p = property_from_data(name=name, required=required, data=data) - - assert p == PropertyError(data=data, detail="type array must have items defined") - - 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) - - from openapi_python_client.parser.properties import property_from_data - - p = property_from_data(name=name, required=required, data=data) - - assert p == PropertyError(data=oai.Schema(), detail=f"invalid data in items of array {name}") - - def test_property_from_data_union(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Schema( - anyOf=[{"type": "number", "default": "0.0"}], - oneOf=[ - {"type": "integer", "default": "0"}, - ], - ) - UnionProperty = mocker.patch(f"{MODULE_NAME}.UnionProperty") - FloatProperty = mocker.patch(f"{MODULE_NAME}.FloatProperty") - 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 - - p = property_from_data(name=name, required=required, data=data) - - 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, - default=None, - inner_properties=[FloatProperty.return_value, IntProperty.return_value], - nullable=False, - ) - assert p == UnionProperty.return_value - - def test_property_from_data_union_bad_type(self, mocker): - name = mocker.MagicMock() - required = mocker.MagicMock() - 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 - - p = property_from_data(name=name, required=required, data=data) - - 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): - name = mocker.MagicMock() - required = mocker.MagicMock() - data = oai.Schema.construct(type="string", nullable=mocker.MagicMock()) - StringProperty = mocker.patch(f"{MODULE_NAME}.StringProperty") - - 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 - - # 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 - ) - - 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") - - from openapi_python_client.parser.properties import _string_based_property - - 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 - ) - - 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") - - from openapi_python_client.parser.properties import _string_based_property - - 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() - - _string_based_property( - name=name, - required=required, - data=data, - ) - DateProperty.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable) - - 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") - - from openapi_python_client.parser.properties import _string_based_property - - 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) - - 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 _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 - - # 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 - ) diff --git a/tests/test_openapi_parser/test_responses.py b/tests/test_openapi_parser/test_responses.py deleted file mode 100644 index ce93efb15..000000000 --- a/tests/test_openapi_parser/test_responses.py +++ /dev/null @@ -1,286 +0,0 @@ -import openapi_python_client.schema as oai - -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() diff --git a/tests/test_openapi_parser/test_openapi.py b/tests/test_parser/test_openapi.py similarity index 64% rename from tests/test_openapi_parser/test_openapi.py rename to tests/test_parser/test_openapi.py index 987dfd763..e3caafa9e 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" @@ -10,39 +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, - schemas=Schemas.build.return_value, - 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.schemas == Schemas() + build_schemas.assert_not_called() def test_from_dict_invalid_schema(self, mocker): Schemas = mocker.patch(f"{MODULE_NAME}.Schemas") @@ -67,127 +65,6 @@ 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=True, nullable=False) - 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") - 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() @@ -240,30 +117,34 @@ 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, parent_name="parent") - 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( + 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): - 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, parent_name="parent") - 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( @@ -275,17 +156,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", @@ -296,13 +179,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() @@ -318,7 +207,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"] ) @@ -332,11 +224,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, parent_name="name") parse_multipart_body.assert_called_once_with(request_body) import_string_from_reference.assert_has_calls( [ @@ -351,8 +247,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 = { @@ -369,12 +266,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, parent_name="name"), + mocker.call(status_code=404, data=response_2_data, schemas=schemas, parent_name="name"), + ] ) assert response.errors == [ ParseError( @@ -388,7 +288,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() @@ -405,34 +306,42 @@ 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, 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] - 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 + from openapi_python_client.parser.openapi import Endpoint, Schemas endpoint = Endpoint( path="path", @@ -442,8 +351,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 @@ -456,15 +369,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", @@ -474,11 +394,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 @@ -502,8 +426,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() @@ -523,14 +451,21 @@ 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, 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="...") @@ -540,6 +475,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 @@ -547,17 +483,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 @@ -565,42 +503,56 @@ 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") - _add_responses = mocker.patch.object(Endpoint, "_add_responses", return_value=parse_error) + param_schemas = mocker.MagicMock() + _add_parameters = mocker.patch.object( + Endpoint, "_add_parameters", return_value=(mocker.MagicMock(), param_schemas) + ) + 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(), 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, response_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") - _add_responses = mocker.patch.object(Endpoint, "_add_responses") - _add_body = mocker.patch.object(Endpoint, "_add_body") + param_schemas = mocker.MagicMock() + param_endpoint = mocker.MagicMock() + _add_parameters = mocker.patch.object(Endpoint, "_add_parameters", return_value=(param_endpoint, param_schemas)) + 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)) 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, @@ -608,34 +560,39 @@ 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(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 path = "/path/with/{param}/" method = "get" - _add_parameters = mocker.patch.object(Endpoint, "_add_parameters") - _add_responses = mocker.patch.object(Endpoint, "_add_responses") - _add_body = mocker.patch.object(Endpoint, "_add_body") + _add_parameters = mocker.patch.object( + Endpoint, "_add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock()) + ) + _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(), 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, @@ -643,10 +600,15 @@ 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( + 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[0], data=data, schemas=_add_responses.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 @@ -657,17 +619,22 @@ def test_from_data_no_security(self, mocker): security=None, responses=mocker.MagicMock(), ) - _add_parameters = mocker.patch.object(Endpoint, "_add_parameters") - _add_responses = mocker.patch.object(Endpoint, "_add_responses") - _add_body = mocker.patch.object(Endpoint, "_add_body") + _add_parameters = mocker.patch.object( + Endpoint, "_add_parameters", return_value=(mocker.MagicMock(), mocker.MagicMock()) + ) + _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() 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, @@ -675,10 +642,15 @@ def test_from_data_no_security(self, mocker): requires_security=False, tag="a", ), - data, + data=data, + schemas=schemas, + ) + _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[0], data=data, schemas=_add_responses.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: @@ -716,23 +688,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 @@ -744,21 +725,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/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 new file mode 100644 index 000000000..80bd74858 --- /dev/null +++ b/tests/test_parser/test_properties/test_init.py @@ -0,0 +1,1144 @@ +import pytest + +import openapi_python_client.schema as oai +from openapi_python_client.parser.errors import PropertyError, ValidationError +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" + + +class TestProperty: + 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) + + base_type_string = f"TestType" + + assert p.get_type_string() == base_type_string + + 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 = 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 = 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 + + 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) + + assert p.to_string() == f"{name}: {get_type_string()}" + + p = Property(name=name, required=False, default=None, nullable=False) + assert p.to_string() == f"{name}: {get_type_string()} = UNSET" + + p = Property(name=name, required=True, default=None, nullable=False) + assert p.to_string() == f"{name}: {get_type_string()}" + + 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): + from openapi_python_client.parser.properties import Property + + p = Property(name="test", required=True, default=None, nullable=False) + assert p.get_imports(prefix="") == set() + + 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 = 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", + } + + +class TestStringProperty: + def test_get_type_string(self): + from openapi_python_client.parser.properties import StringProperty + + p = StringProperty(name="test", required=True, default=None, nullable=False) + + base_type_string = f"str" + + assert p.get_type_string() == base_type_string + + p = StringProperty(name="test", required=True, default=None, nullable=True) + assert p.get_type_string() == f"Optional[{base_type_string}]" + + p = StringProperty(name="test", required=False, default=None, nullable=True) + assert p.get_type_string() == f"Union[Unset, Optional[{base_type_string}]]" + + p = StringProperty(name="test", required=False, default=None, nullable=False) + assert p.get_type_string() == f"Union[Unset, {base_type_string}]" + + +class TestDateTimeProperty: + def test_get_imports(self): + from openapi_python_client.parser.properties import DateTimeProperty + + p = DateTimeProperty(name="test", required=True, default=None, nullable=False) + assert p.get_imports(prefix="...") == { + "import datetime", + "from typing import cast", + "from dateutil.parser import isoparse", + } + + p = DateTimeProperty(name="test", required=False, default=None, nullable=False) + assert p.get_imports(prefix="...") == { + "import datetime", + "from typing import cast", + "from dateutil.parser import isoparse", + "from typing import Union", + "from ...types import UNSET, Unset", + } + + p = DateTimeProperty(name="test", required=False, default=None, nullable=True) + assert p.get_imports(prefix="...") == { + "import datetime", + "from typing import cast", + "from dateutil.parser import isoparse", + "from typing import Union", + "from typing import Optional", + "from ...types import UNSET, Unset", + } + + +class TestDateProperty: + def test_get_imports(self): + from openapi_python_client.parser.properties import DateProperty + + p = DateProperty(name="test", required=True, default=None, nullable=False) + assert p.get_imports(prefix="...") == { + "import datetime", + "from typing import cast", + "from dateutil.parser import isoparse", + } + + p = DateProperty(name="test", required=False, default=None, nullable=False) + assert p.get_imports(prefix="...") == { + "import datetime", + "from typing import cast", + "from dateutil.parser import isoparse", + "from typing import Union", + "from ...types import UNSET, Unset", + } + + p = DateProperty(name="test", required=False, default=None, nullable=True) + assert p.get_imports(prefix="...") == { + "import datetime", + "from typing import cast", + "from dateutil.parser import isoparse", + "from typing import Union", + "from typing import Optional", + "from ...types import UNSET, Unset", + } + + +class TestFileProperty: + def test_get_imports(self): + from openapi_python_client.parser.properties import FileProperty + + 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", + } + + 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", + "from ...types import UNSET, Unset", + } + + +class TestListProperty: + def test_get_type_string(self, mocker): + from openapi_python_client.parser.properties import ListProperty + + inner_property = mocker.MagicMock() + inner_type_string = mocker.MagicMock() + inner_property.get_type_string.return_value = inner_type_string + p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=False) + + base_type_string = f"List[{inner_type_string}]" + + assert p.get_type_string() == base_type_string + + 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 = 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 = 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 + + def test_get_type_imports(self, mocker): + from openapi_python_client.parser.properties import ListProperty + + inner_property = mocker.MagicMock() + inner_import = mocker.MagicMock() + inner_property.get_imports.return_value = {inner_import} + 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 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 cast, List", + "from typing import Union", + "from ...types import UNSET, Unset", + } + + 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 cast, List", + "from typing import Union", + "from typing import Optional", + "from ...types import UNSET, Unset", + } + + +class TestUnionProperty: + def test_get_type_string(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_2 = mocker.MagicMock() + inner_property_2.get_type_string.return_value = "inner_type_string_2" + p = UnionProperty( + name="test", + required=True, + default=None, + inner_properties=[inner_property_1, inner_property_2], + nullable=False, + ) + + base_type_string = f"Union[inner_type_string_1, inner_type_string_2]" + + assert p.get_type_string() == base_type_string + + 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 = 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 = 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 + + def test_get_type_imports(self, mocker): + from openapi_python_client.parser.properties import UnionProperty + + inner_property_1 = mocker.MagicMock() + inner_import_1 = mocker.MagicMock() + inner_property_1.get_imports.return_value = {inner_import_1} + inner_property_2 = mocker.MagicMock() + inner_import_2 = mocker.MagicMock() + inner_property_2.get_imports.return_value = {inner_import_2} + prefix = "..." + p = UnionProperty( + name="test", + required=True, + default=None, + inner_properties=[inner_property_1, inner_property_2], + nullable=False, + ) + + assert p.get_imports(prefix=prefix) == { + inner_import_1, + inner_import_2, + "from typing import Union", + } + + 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, + "from typing import Union", + "from ...types import UNSET, Unset", + } + + 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, + "from typing import Union", + "from typing import Optional", + "from ...types import UNSET, Unset", + } + + +class TestEnumProperty: + def test_get_type_string(self, mocker): + fake_reference = mocker.MagicMock(class_name="MyTestEnum") + + from openapi_python_client.parser import properties + + p = properties.EnumProperty( + 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 = 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 = 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 = 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") + prefix = "..." + + from openapi_python_client.parser import properties + + enum_property = properties.EnumProperty( + 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 = 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 = 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", + "from typing import Optional", + "from ...types import UNSET, Unset", + } + + def test_values_from_list(self): + from openapi_python_client.parser.properties import EnumProperty + + data = ["abc", "123", "a23", "1bc", 4, -3, "a Thing WIth spaces"] + + result = EnumProperty.values_from_list(data) + + assert result == { + "ABC": "abc", + "VALUE_1": "123", + "A23": "a23", + "VALUE_3": "1bc", + "VALUE_4": 4, + "VALUE_NEGATIVE_3": -3, + "A_THING_WITH_SPACES": "a Thing WIth spaces", + } + + def test_values_from_list_duplicate(self): + from openapi_python_client.parser.properties import EnumProperty + + data = ["abc", "123", "a23", "abc"] + + with pytest.raises(ValueError): + EnumProperty.values_from_list(data) + + +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 + + 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, parent_name="parent" + ) + + assert prop == EnumProperty( + name="my_enum", + required=True, + nullable=False, + values={"A": "A", "B": "B", "C": "C"}, + reference=Reference(class_name="ParentAnEnum", module_name="parent_an_enum"), + value_type=str, + default="ParentAnEnum.B", + ) + assert schemas != new_schemas, "Provided Schemas was mutated" + assert new_schemas.enums == { + "AnEnum": schemas.enums["AnEnum"], + "ParentAnEnum": 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 + + 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={"AnEnum": mocker.MagicMock()}) + + 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="ParentAnEnum", module_name="parent_an_enum"), + value_type=int, + default="ParentAnEnum.VALUE_3", + ) + assert schemas != new_schemas, "Provided Schemas was mutated" + assert new_schemas.enums == { + "AnEnum": schemas.enums["AnEnum"], + "ParentAnEnum": prop, + } + + 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") + existing_enum = EnumProperty( + name="an_enum", + required=True, + nullable=False, + default=None, + values={"A": "a"}, + value_type=str, + reference=Reference(class_name="MyEnum", module_name="my_enum"), + ) + schemas = Schemas(enums={"MyEnum": existing_enum}) + + prop, new_schemas = property_from_data(name=name, required=False, data=data, schemas=schemas, parent_name="") + + assert prop == EnumProperty( + name="some_enum", + required=False, + nullable=False, + default=None, + values={"A": "a"}, + value_type=str, + reference=Reference(class_name="MyEnum", module_name="my_enum"), + ) + assert schemas == new_schemas + + def test_property_from_data_ref_model(self): + 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, parent_name="") + + 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 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, 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") + 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() + + 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 + _string_based_property.assert_called_once_with(name=name, required=required, data=data) + + @pytest.mark.parametrize( + "openapi_type,prop_type,python_type", + [ + ("number", FloatProperty, float), + ("integer", IntProperty, int), + ("boolean", BooleanProperty, bool), + ], + ) + 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 = "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, parent_name="parent" + ) + + assert p == prop_type(name=name, required=required, default=python_type(data.default), nullable=False) + assert new_schemas == schemas + + # Test nullable values + data.default = 0 + data.nullable = True + + 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, parent_name="parent") + 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 + + name = mocker.MagicMock() + required = mocker.MagicMock() + data = oai.Schema( + type="array", + items={"type": "number", "default": "0.0"}, + ) + 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() + + 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, 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 + + name = mocker.MagicMock() + required = mocker.MagicMock() + data = oai.Schema.construct( + anyOf=[{"type": "number", "default": "0.0"}], + oneOf=[ + {"type": "integer", "default": "0"}, + ], + ) + 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, 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, parent_name="parent" + ) + + 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 Schemas, property_from_data + + 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(), + ) + + def test_property_from_data_no_valid_props_in_data(self): + from openapi_python_client.parser.properties import NoneProperty, Schemas, property_from_data + + schemas = Schemas() + data = oai.Schema() + + 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 + + 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, parent_name="parent" + ) + 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, parent_name="parent" + ) + + 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 + + name = "name" + 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=(properties.PropertyError(data="blah"), second_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, parent_name="parent_name" + ) + + def test_build_list_property(self, mocker): + from openapi_python_client.parser import properties + + name = "prop" + 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, 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, parent_name="parent_prop" + ) + + +class TestBuildUnionProperty: + def test_property_from_data_union(self, mocker): + name = mocker.MagicMock() + required = mocker.MagicMock() + data = oai.Schema( + anyOf=[{"type": "number", "default": "0.0"}], + oneOf=[ + {"type": "integer", "default": "0"}, + ], + ) + UnionProperty = mocker.patch(f"{MODULE_NAME}.UnionProperty") + FloatProperty = mocker.patch(f"{MODULE_NAME}.FloatProperty") + 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 Schemas, property_from_data + + 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) + UnionProperty.assert_called_once_with( + name=name, + required=required, + default=None, + inner_properties=[FloatProperty.return_value, IntProperty.return_value], + nullable=False, + ) + assert p == UnionProperty.return_value + assert s == Schemas() + + def test_property_from_data_union_bad_type(self, mocker): + name = "bad_union" + required = mocker.MagicMock() + 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(), parent_name="parent") + + assert p == PropertyError(detail=f"Invalid property in union {name}", data=oai.Schema(type="garbage")) + + +class TestStringBasedProperty: + 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) + + assert p == StringProperty(name=name, required=required, nullable=True, default="'\\\\\"hello world\\\\\"'") + + data.pattern = "abcdef" + data.nullable = False + + p = _string_based_property( + name=name, + required=required, + data=data, + ) + assert p == StringProperty( + name=name, required=required, nullable=False, default="'\\\\\"hello world\\\\\"'", pattern="abcdef" + ) + + def test__string_based_property_datetime_format(self): + from openapi_python_client.parser.properties import DateTimeProperty, _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) + + assert p == DateTimeProperty( + name=name, required=required, nullable=True, default="isoparse('2020-11-06T12:00:00')" + ) + + # Test bad default + data.default = "a" + with pytest.raises(ValidationError): + _string_based_property(name=name, required=required, data=data) + + 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) + + assert p == DateProperty(name=name, required=required, nullable=True, default="isoparse('2020-11-06').date()") + + # 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): + from openapi_python_client.parser.properties import FileProperty, _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) + assert p == FileProperty(name=name, required=required, nullable=True, default=None) + + def test__string_based_property_unsupported_format(self, mocker): + from openapi_python_client.parser.properties import StringProperty, _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) + + assert p == StringProperty(name=name, required=required, nullable=True, default=None) + + +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(errors=[]) + error = PropertyError() + 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 + + result = build_schemas(components=in_data) + + build_model_property.assert_has_calls( + [ + 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 + assert result == schemas_2 + 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() + + +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_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_parser/test_responses.py b/tests/test_parser/test_responses.py new file mode 100644 index 000000000..eb20fb338 --- /dev/null +++ b/tests/test_parser/test_responses.py @@ -0,0 +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" + + +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" + ) 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()