Skip to content

Commit

Permalink
[python] Adds python oneOf/anyOf models + tests (OpenAPITools#5341)
Browse files Browse the repository at this point in the history
* Adds oneOf + anyOf schemas, models and tests to python-experimental

* Adds setUpClass and tearDownClass

* Removes newline in method_init_shared.mustache

* Regenerated v3 spec sample for python-experimental

* Fxes test for discard_unknown_keys

* Moves new models into existing spec, regen python-exp and go-exp

* Also fix python-exp windows file
  • Loading branch information
spacether authored Feb 28, 2020
1 parent 857a4bf commit 9e59669
Show file tree
Hide file tree
Showing 186 changed files with 5,342 additions and 204 deletions.
2 changes: 1 addition & 1 deletion bin/openapi3/windows/python-experimental-petstore.bat
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ If Not Exist %executable% (
)

REM set JAVA_OPTS=%JAVA_OPTS% -Xmx1024M
set ags=generate -i modules\openapi-generator\src\test\resources\3_0\petstore-with-fake-endpoints-models-for-testing.yaml -g python-experimental -o samples\openapi3\client\petstore\python-experimental --additional-properties packageName=petstore_api
set ags=generate -i modules\openapi-generator\src\test\resources\3_0\petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml -g python-experimental -o samples\openapi3\client\petstore\python-experimental --additional-properties packageName=petstore_api

java %JAVA_OPTS% -jar %executable% %ags%
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,56 @@ public void postProcessParameter(CodegenParameter p) {
}
}

private void addNullDefaultToOneOfAnyOfReqProps(Schema schema, CodegenModel result){
// for composed schema models, if the required properties are only from oneOf or anyOf models
// give them a nulltype.Null so the user can omit including them in python
ComposedSchema cs = (ComposedSchema) schema;

// these are the properties that are from properties in self cs or cs allOf
Map<String, Schema> selfProperties = new LinkedHashMap<String, Schema>();
List<String> selfRequired = new ArrayList<String>();

// these are the properties that are from properties in cs oneOf or cs anyOf
Map<String, Schema> otherProperties = new LinkedHashMap<String, Schema>();
List<String> otherRequired = new ArrayList<String>();

List<Schema> oneOfanyOfSchemas = new ArrayList<>();
List<Schema> oneOf = cs.getOneOf();
if (oneOf != null) {
oneOfanyOfSchemas.addAll(oneOf);
}
List<Schema> anyOf = cs.getAnyOf();
if (anyOf != null) {
oneOfanyOfSchemas.addAll(anyOf);
}
for (Schema sc: oneOfanyOfSchemas) {
Schema refSchema = ModelUtils.getReferencedSchema(this.openAPI, sc);
addProperties(otherProperties, otherRequired, refSchema);
}
Set<String> otherRequiredSet = new HashSet<String>(otherRequired);

List<Schema> allOf = cs.getAllOf();
if ((schema.getProperties() != null && !schema.getProperties().isEmpty()) || allOf != null) {
// NOTE: this function also adds the allOf propesrties inside schema
addProperties(selfProperties, selfRequired, schema);
}
if (result.discriminator != null) {
selfRequired.add(result.discriminator.getPropertyBaseName());
}
Set<String> selfRequiredSet = new HashSet<String>(selfRequired);

List<CodegenProperty> reqVars = result.getRequiredVars();
if (reqVars != null) {
for (CodegenProperty cp: reqVars) {
String propName = cp.baseName;
if (otherRequiredSet.contains(propName) && !selfRequiredSet.contains(propName)) {
// if var is in otherRequiredSet and is not in selfRequiredSet and is in result.requiredVars
// then set it to nullable because the user doesn't have to give a value for it
cp.setDefaultValue("nulltype.Null");
}
}
}
}

/**
* Convert OAS Model object to Codegen Model object
Expand Down Expand Up @@ -806,6 +856,11 @@ public CodegenModel fromModel(String name, Schema schema) {
if (result.imports.contains(result.classname)) {
result.imports.remove(result.classname);
}

if (result.requiredVars.size() > 0 && (result.oneOf.size() > 0 || result.anyOf.size() > 0)) {
addNullDefaultToOneOfAnyOfReqProps(schema, result);
}

return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import re # noqa: F401
import sys # noqa: F401

import six # noqa: F401
import nulltype # noqa: F401

from {{packageName}}.model_utils import ( # noqa: F401
ModelComposed,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@
'_from_server': _from_server,
'_configuration': _configuration,
}
model_args = {
required_args = {
{{#requiredVars}}
'{{name}}': {{name}},
{{/requiredVars}}
}
# remove args whose value is Null because they are unset
required_arg_names = list(required_args.keys())
for required_arg_name in required_arg_names:
if required_args[required_arg_name] is nulltype.Null:
del required_args[required_arg_name]
model_args = {}
model_args.update(required_args)
model_args.update(kwargs)
composed_info = validate_get_composed_info(
constant_args, model_args, self)
Expand All @@ -30,9 +37,8 @@
self._additional_properties_model_instances = composed_info[2]
unused_args = composed_info[3]

{{#requiredVars}}
self.{{name}} = {{name}}
{{/requiredVars}}
for var_name, var_value in required_args.items():
setattr(self, var_name, var_value)
for var_name, var_value in six.iteritems(kwargs):
if var_name in unused_args and \
self._configuration is not None and \
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
def __init__(self{{#requiredVars}}{{^defaultValue}}, {{name}}{{/defaultValue}}{{/requiredVars}}{{#requiredVars}}{{#defaultValue}}, {{name}}={{{defaultValue}}}{{/defaultValue}}{{/requiredVars}}, _check_type=True, _from_server=False, _path_to_item=(), _configuration=None, **kwargs): # noqa: E501
"""{{classname}} - a model defined in OpenAPI

{{#requiredVars}}{{^hasMore}} Args:{{/hasMore}}{{/requiredVars}}{{#requiredVars}}{{^defaultValue}}
{{name}} ({{{dataType}}}):{{#description}} {{description}}{{/description}}{{/defaultValue}}{{/requiredVars}}{{#requiredVars}}{{^hasMore}}
{{/hasMore}}{{/requiredVars}}
Keyword Args:{{#requiredVars}}{{#defaultValue}}
{{name}} ({{{dataType}}}):{{#description}} {{description}}.{{/description}} defaults to {{{defaultValue}}}, must be one of [{{{defaultValue}}}] # noqa: E501{{/defaultValue}}{{/requiredVars}}
{{#requiredVars}}
{{#-first}}
Args:
{{/-first}}
{{^defaultValue}}
{{name}} ({{{dataType}}}):{{#description}} {{description}}{{/description}}
{{/defaultValue}}
{{#-last}}

{{/-last}}
{{/requiredVars}}
Keyword Args:
{{#requiredVars}}
{{#defaultValue}}
{{name}} ({{{dataType}}}):{{#description}} {{description}}.{{/description}} defaults to {{{defaultValue}}}{{#allowableValues}}, must be one of [{{#enumVars}}{{{value}}}, {{/enumVars}}]{{/allowableValues}} # noqa: E501
{{/defaultValue}}
{{/requiredVars}}
_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.
Expand All @@ -18,8 +30,10 @@
_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.{{#optionalVars}}
{{name}} ({{{dataType}}}):{{#description}} {{description}}.{{/description}} [optional]{{#defaultValue}} if omitted the server will use the default value of {{{defaultValue}}}{{/defaultValue}} # noqa: E501{{/optionalVars}}
If omitted no type conversion is done.
{{#optionalVars}}
{{name}} ({{{dataType}}}):{{#description}} {{description}}.{{/description}} [optional]{{#defaultValue}} if omitted the server will use the default value of {{{defaultValue}}}{{/defaultValue}} # noqa: E501
{{/optionalVars}}
"""

self._data_store = {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,23 @@
if self._path_to_item:
path_to_item.extend(self._path_to_item)
path_to_item.append(name)
values = set()
if model_instances:
values = set()
for model_instance in model_instances:
if name in model_instance._data_store:
values.add(model_instance._data_store[name])
if len(values) == 1:
return list(values)[0]
len_values = len(values)
if len_values == 0:
raise ApiKeyError(
"{0} has no key '{1}'".format(type(self).__name__, name),
path_to_item
)
elif len_values == 1:
return list(values)[0]
elif len_values > 1:
raise ApiValueError(
"Values stored for property {0} in {1} difffer when looking "
"at self and self's composed instances. All values must be "
"the same".format(name, type(self).__name__),
path_to_item
)

raise ApiKeyError(
"{0} has no key '{1}'".format(type(self).__name__, name),
path_to_item
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,7 @@ def model_to_dict(model_instance, serialize=True):

model_instances = [model_instance]
if model_instance._composed_schemas() is not None:
model_instances = model_instance._composed_instances
model_instances.extend(model_instance._composed_instances)
for model_instance in model_instances:
for attr, value in six.iteritems(model_instance._data_store):
if serialize:
Expand Down Expand Up @@ -951,12 +951,12 @@ def get_oneof_instance(self, model_args, constant_args):
used to make instances

Returns
oneof_instance (instance)
oneof_instance (instance/None)
"""
oneof_instance = None
if len(self._composed_schemas()['oneOf']) == 0:
return oneof_instance
return None

oneof_instances = []
for oneof_class in self._composed_schemas()['oneOf']:
# transform js keys to python keys in fixed_model_args
fixed_model_args = change_keys_js_to_python(
Expand All @@ -969,20 +969,30 @@ def get_oneof_instance(self, model_args, constant_args):
if var_name in fixed_model_args:
kwargs[var_name] = fixed_model_args[var_name]

# do not try to make a model with no input args
if len(kwargs) == 0:
continue

# and use it to make the instance
kwargs.update(constant_args)
try:
oneof_instance = oneof_class(**kwargs)
break
oneof_instances.append(oneof_instance)
except Exception:
pass
if oneof_instance is None:
if len(oneof_instances) == 0:
raise ApiValueError(
"Invalid inputs given to generate an instance of %s. Unable to "
"make any instances of the classes in oneOf definition." %
self.__class__.__name__
)
return oneof_instance
elif len(oneof_instances) > 1:
raise ApiValueError(
"Invalid inputs given to generate an instance of %s. Multiple "
"oneOf instances were generated when a max of one is allowed." %
self.__class__.__name__
)
return oneof_instances[0]


def get_anyof_instances(self, model_args, constant_args):
Expand Down Expand Up @@ -1012,6 +1022,10 @@ def get_anyof_instances(self, model_args, constant_args):
if var_name in fixed_model_args:
kwargs[var_name] = fixed_model_args[var_name]

# do not try to make a model with no input args
if len(kwargs) == 0:
continue

# and use it to make the instance
kwargs.update(constant_args)
try:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
nulltype
certifi >= 14.05.14
future; python_version<="2.7"
six >= 1.10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ REQUIRES = [
"six >= 1.10",
"certifi",
"python-dateutil",
"nulltype",
{{#asyncio}}
"aiohttp >= 3.0.0",
{{/asyncio}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1779,3 +1779,82 @@ components:
additionalProperties:
type: object
nullable: true
fruit:
properties:
color:
type: string
oneOf:
- $ref: '#/components/schemas/apple'
- $ref: '#/components/schemas/banana'
apple:
type: object
properties:
cultivar:
type: string
banana:
type: object
properties:
lengthCm:
type: number
mammal:
oneOf:
- $ref: '#/components/schemas/whale'
- $ref: '#/components/schemas/zebra'
discriminator:
propertyName: className
mapping:
whale: '#/components/schemas/whale'
zebra: '#/components/schemas/zebra'
whale:
type: object
properties:
hasBaleen:
type: boolean
hasTeeth:
type: boolean
className:
type: string
required:
- className
zebra:
type: object
properties:
type:
type: string
enum:
- plains
- mountain
- grevys
className:
type: string
required:
- className
gmFruit:
properties:
color:
type: string
anyOf:
- $ref: '#/components/schemas/apple'
- $ref: '#/components/schemas/banana'
fruitReq:
oneOf:
- $ref: '#/components/schemas/appleReq'
- $ref: '#/components/schemas/bananaReq'
appleReq:
type: object
properties:
cultivar:
type: string
mealy:
type: boolean
required:
- cultivar
bananaReq:
type: object
properties:
lengthCm:
type: number
sweet:
type: boolean
required:
- lengthCm
Loading

0 comments on commit 9e59669

Please sign in to comment.