From 1a6cc67fccfdda7015985882b09497726640b61d Mon Sep 17 00:00:00 2001 From: Sebastien Rosset Date: Wed, 13 May 2020 09:11:43 -0700 Subject: [PATCH] [python-experimental] Support schema property which has $ref to 'oneOf' schema (#6262) * Add reference to oneOf schema * Add model showing unit test failure with ref to oneOf schema * Updates get_discriminator_class to return visited_composed_classes * Fixes broken test, adds is_valid_type * move unit test to test_drawing.py file * Add more unit tests * invoke git pull from spacether fork * invoke git pull from spacether fork * Improve unit tests Co-authored-by: Justin Black --- .../python-experimental/model_utils.mustache | 27 ++- ...odels-for-testing-with-http-signature.yaml | 11 ++ .../petstore_api/model_utils.py | 27 ++- .../petstore/python-experimental/README.md | 1 + .../python-experimental/docs/Drawing.md | 11 ++ .../petstore_api/__init__.py | 1 + .../petstore_api/model_utils.py | 27 ++- .../petstore_api/models/drawing.py | 160 ++++++++++++++++++ .../python-experimental/test/test_drawing.py | 68 ++++++++ 9 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 samples/openapi3/client/petstore/python-experimental/docs/Drawing.md create mode 100644 samples/openapi3/client/petstore/python-experimental/petstore_api/models/drawing.py create mode 100644 samples/openapi3/client/petstore/python-experimental/test/test_drawing.py diff --git a/modules/openapi-generator/src/main/resources/python/python-experimental/model_utils.mustache b/modules/openapi-generator/src/main/resources/python/python-experimental/model_utils.mustache index fcbb59e7915..3c99e7fb992 100644 --- a/modules/openapi-generator/src/main/resources/python/python-experimental/model_utils.mustache +++ b/modules/openapi-generator/src/main/resources/python/python-experimental/model_utils.mustache @@ -872,6 +872,31 @@ def attempt_convert_item(input_value, valid_classes, path_to_item, return input_value +def is_valid_type(input_class_simple, valid_classes): + """ + Args: + input_class_simple (class): the class of the input_value that we are + checking + valid_classes (tuple): the valid classes that the current item + should be + Returns: + bool + """ + valid_type = input_class_simple in valid_classes + if not valid_type and issubclass(input_class_simple, OpenApiModel): + for valid_class in valid_classes: + if not valid_class.discriminator: + continue + discr_propertyname_py = list(valid_class.discriminator.keys())[0] + discriminator_classes = ( + valid_class.discriminator[discr_propertyname_py].values() + ) + valid_type = is_valid_type(input_class_simple, discriminator_classes) + if valid_type: + return True + return valid_type + + def validate_and_convert_types(input_value, required_types_mixed, path_to_item, from_server, _check_type, configuration=None): """Raises a TypeError is there is a problem, otherwise returns value @@ -904,7 +929,7 @@ def validate_and_convert_types(input_value, required_types_mixed, path_to_item, valid_classes, child_req_types_by_current_type = results input_class_simple = get_simple_class(input_value) - valid_type = input_class_simple in set(valid_classes) + valid_type = is_valid_type(input_class_simple, valid_classes) if not valid_type: if configuration: # if input_value is not valid_type try to convert it diff --git a/modules/openapi-generator/src/test/resources/3_0/python-experimental/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml b/modules/openapi-generator/src/test/resources/3_0/python-experimental/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml index edc4ca0e641..f617a76f204 100644 --- a/modules/openapi-generator/src/test/resources/3_0/python-experimental/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/python-experimental/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml @@ -1892,6 +1892,17 @@ components: - lengthCm # go-experimental is unable to make Triangle and Quadrilateral models # correctly https://github.com/OpenAPITools/openapi-generator/issues/6149 + Drawing: + type: object + properties: + mainShape: + # A property whose value is a 'oneOf' type, and the type is referenced instead + # of being define inline. + $ref: '#/components/schemas/Shape' + shapes: + type: array + items: + $ref: '#/components/schemas/Shape' Shape: oneOf: - $ref: '#/components/schemas/Triangle' diff --git a/samples/client/petstore/python-experimental/petstore_api/model_utils.py b/samples/client/petstore/python-experimental/petstore_api/model_utils.py index b420b770d28..a954b14af67 100644 --- a/samples/client/petstore/python-experimental/petstore_api/model_utils.py +++ b/samples/client/petstore/python-experimental/petstore_api/model_utils.py @@ -1138,6 +1138,31 @@ def attempt_convert_item(input_value, valid_classes, path_to_item, return input_value +def is_valid_type(input_class_simple, valid_classes): + """ + Args: + input_class_simple (class): the class of the input_value that we are + checking + valid_classes (tuple): the valid classes that the current item + should be + Returns: + bool + """ + valid_type = input_class_simple in valid_classes + if not valid_type and issubclass(input_class_simple, OpenApiModel): + for valid_class in valid_classes: + if not valid_class.discriminator: + continue + discr_propertyname_py = list(valid_class.discriminator.keys())[0] + discriminator_classes = ( + valid_class.discriminator[discr_propertyname_py].values() + ) + valid_type = is_valid_type(input_class_simple, discriminator_classes) + if valid_type: + return True + return valid_type + + def validate_and_convert_types(input_value, required_types_mixed, path_to_item, from_server, _check_type, configuration=None): """Raises a TypeError is there is a problem, otherwise returns value @@ -1170,7 +1195,7 @@ def validate_and_convert_types(input_value, required_types_mixed, path_to_item, valid_classes, child_req_types_by_current_type = results input_class_simple = get_simple_class(input_value) - valid_type = input_class_simple in set(valid_classes) + valid_type = is_valid_type(input_class_simple, valid_classes) if not valid_type: if configuration: # if input_value is not valid_type try to convert it diff --git a/samples/openapi3/client/petstore/python-experimental/README.md b/samples/openapi3/client/petstore/python-experimental/README.md index 8dd6d472418..91c2a72634b 100644 --- a/samples/openapi3/client/petstore/python-experimental/README.md +++ b/samples/openapi3/client/petstore/python-experimental/README.md @@ -147,6 +147,7 @@ Class | Method | HTTP request | Description - [complex_quadrilateral.ComplexQuadrilateral](docs/ComplexQuadrilateral.md) - [dog.Dog](docs/Dog.md) - [dog_all_of.DogAllOf](docs/DogAllOf.md) + - [drawing.Drawing](docs/Drawing.md) - [enum_arrays.EnumArrays](docs/EnumArrays.md) - [enum_class.EnumClass](docs/EnumClass.md) - [enum_test.EnumTest](docs/EnumTest.md) diff --git a/samples/openapi3/client/petstore/python-experimental/docs/Drawing.md b/samples/openapi3/client/petstore/python-experimental/docs/Drawing.md new file mode 100644 index 00000000000..7c805f9e0fa --- /dev/null +++ b/samples/openapi3/client/petstore/python-experimental/docs/Drawing.md @@ -0,0 +1,11 @@ +# drawing.Drawing + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**main_shape** | [**shape.Shape**](Shape.md) | | [optional] +**shapes** | [**[shape.Shape]**](Shape.md) | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/__init__.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/__init__.py index 2cd5a39870e..e9cefc31908 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/__init__.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/__init__.py @@ -65,6 +65,7 @@ from petstore_api.models.client import Client from petstore_api.models.complex_quadrilateral import ComplexQuadrilateral from petstore_api.models.dog import Dog from petstore_api.models.dog_all_of import DogAllOf +from petstore_api.models.drawing import Drawing from petstore_api.models.enum_arrays import EnumArrays from petstore_api.models.enum_class import EnumClass from petstore_api.models.enum_test import EnumTest diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/model_utils.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/model_utils.py index b420b770d28..a954b14af67 100644 --- a/samples/openapi3/client/petstore/python-experimental/petstore_api/model_utils.py +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/model_utils.py @@ -1138,6 +1138,31 @@ def attempt_convert_item(input_value, valid_classes, path_to_item, return input_value +def is_valid_type(input_class_simple, valid_classes): + """ + Args: + input_class_simple (class): the class of the input_value that we are + checking + valid_classes (tuple): the valid classes that the current item + should be + Returns: + bool + """ + valid_type = input_class_simple in valid_classes + if not valid_type and issubclass(input_class_simple, OpenApiModel): + for valid_class in valid_classes: + if not valid_class.discriminator: + continue + discr_propertyname_py = list(valid_class.discriminator.keys())[0] + discriminator_classes = ( + valid_class.discriminator[discr_propertyname_py].values() + ) + valid_type = is_valid_type(input_class_simple, discriminator_classes) + if valid_type: + return True + return valid_type + + def validate_and_convert_types(input_value, required_types_mixed, path_to_item, from_server, _check_type, configuration=None): """Raises a TypeError is there is a problem, otherwise returns value @@ -1170,7 +1195,7 @@ def validate_and_convert_types(input_value, required_types_mixed, path_to_item, valid_classes, child_req_types_by_current_type = results input_class_simple = get_simple_class(input_value) - valid_type = input_class_simple in set(valid_classes) + valid_type = is_valid_type(input_class_simple, valid_classes) if not valid_type: if configuration: # if input_value is not valid_type try to convert it diff --git a/samples/openapi3/client/petstore/python-experimental/petstore_api/models/drawing.py b/samples/openapi3/client/petstore/python-experimental/petstore_api/models/drawing.py new file mode 100644 index 00000000000..e47ede9cc92 --- /dev/null +++ b/samples/openapi3/client/petstore/python-experimental/petstore_api/models/drawing.py @@ -0,0 +1,160 @@ +# coding: utf-8 + +""" + OpenAPI Petstore + + This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\ # noqa: E501 + + The version of the OpenAPI document: 1.0.0 + Generated by: https://openapi-generator.tech +""" + + +from __future__ import absolute_import +import re # noqa: F401 +import sys # noqa: F401 + +import six # noqa: F401 +import nulltype # noqa: F401 + +from petstore_api.model_utils import ( # noqa: F401 + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + int, + none_type, + str, + validate_get_composed_info, +) +try: + from petstore_api.models import shape +except ImportError: + shape = sys.modules[ + 'petstore_api.models.shape'] + + +class Drawing(ModelNormal): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + discriminator_value_class_map (dict): A dict to go from the discriminator + variable value to the discriminator class name. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = { + } + + validations = { + } + + additional_properties_type = None + + @cached_property + def openapi_types(): + """ + This must be a class method so a model may have properties that are + of type self, this ensures that we don't create a cyclic import + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + return { + 'main_shape': (shape.Shape,), # noqa: E501 + 'shapes': ([shape.Shape],), # noqa: E501 + } + + @cached_property + def discriminator(): + return None + + attribute_map = { + 'main_shape': 'mainShape', # noqa: E501 + 'shapes': 'shapes', # noqa: E501 + } + + _composed_schemas = {} + + required_properties = set([ + '_data_store', + '_check_type', + '_from_server', + '_path_to_item', + '_configuration', + '_visited_composed_classes', + ]) + + @convert_js_args_to_python_args + def __init__(self, _check_type=True, _from_server=False, _path_to_item=(), _configuration=None, _visited_composed_classes=(), **kwargs): # noqa: E501 + """drawing.Drawing - a model defined in OpenAPI + + Keyword Args: + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _from_server (bool): True if the data is from the server + False if the data is from the client (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + main_shape (shape.Shape): [optional] # noqa: E501 + shapes ([shape.Shape]): [optional] # noqa: E501 + """ + + self._data_store = {} + self._check_type = _check_type + self._from_server = _from_server + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + + for var_name, var_value in six.iteritems(kwargs): + if var_name not in self.attribute_map and \ + self._configuration is not None and \ + self._configuration.discard_unknown_keys and \ + self.additional_properties_type is None: + # discard variable. + continue + setattr(self, var_name, var_value) diff --git a/samples/openapi3/client/petstore/python-experimental/test/test_drawing.py b/samples/openapi3/client/petstore/python-experimental/test/test_drawing.py new file mode 100644 index 00000000000..230ea4ae7cf --- /dev/null +++ b/samples/openapi3/client/petstore/python-experimental/test/test_drawing.py @@ -0,0 +1,68 @@ +# coding: utf-8 + +""" + OpenAPI Petstore + + This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\ # noqa: E501 + + The version of the OpenAPI document: 1.0.0 + Generated by: https://openapi-generator.tech +""" + + +from __future__ import absolute_import + +import unittest + +import petstore_api + + +class TestDrawing(unittest.TestCase): + """Drawing unit test stubs""" + + def setUp(self): + self.api_client = petstore_api.ApiClient() + + def tearDown(self): + pass + + def test_deserialize_oneof_reference(self): + isosceles_triangle = petstore_api.Shape( + shape_type="Triangle", + triangle_type="IsoscelesTriangle" + ) + assert isinstance(isosceles_triangle, petstore_api.IsoscelesTriangle) + inst = petstore_api.Drawing( + # 'main_shape' has type 'Shape', which is a oneOf [triangle, quadrilateral] + # composed schema. So we should be able to assign a petstore_api.Triangle + # to a 'main_shape'. + main_shape=isosceles_triangle, + shapes=[ + petstore_api.Shape( + shape_type="Triangle", + triangle_type="EquilateralTriangle" + ), + petstore_api.Triangle( + shape_type="Triangle", + triangle_type="IsoscelesTriangle" + ), + petstore_api.EquilateralTriangle( + shape_type="Triangle", + triangle_type="EquilateralTriangle" + ), + petstore_api.Shape( + shape_type="Quadrilateral", + quadrilateral_type="ComplexQuadrilateral" + ), + ], + ) + assert isinstance(inst, petstore_api.Drawing) + assert isinstance(inst.main_shape, petstore_api.IsoscelesTriangle) + self.assertEqual(len(inst.shapes), 4) + assert isinstance(inst.shapes[0], petstore_api.EquilateralTriangle) + assert isinstance(inst.shapes[1], petstore_api.IsoscelesTriangle) + assert isinstance(inst.shapes[2], petstore_api.EquilateralTriangle) + assert isinstance(inst.shapes[3], petstore_api.ComplexQuadrilateral) + +if __name__ == '__main__': + unittest.main()