Python-exp remove codegemodel mutation, allow mixed OneOf types (#6797)

* Stops converting primitive models into object models, adds ComposedSchemas with mixed type

* Samples update for python-exp
This commit is contained in:
Justin Black
2020-07-18 10:13:22 -07:00
committed by GitHub
parent 44d3f717f8
commit ed84280108
52 changed files with 1883 additions and 504 deletions

View File

@@ -62,6 +62,58 @@ class cached_property(object):
return result
PRIMITIVE_TYPES = (list, float, int, bool, datetime, date, str, file_type)
def allows_single_value_input(cls):
"""
This function returns True if the input composed schema model or any
descendant model allows a value only input
This is true for cases where oneOf contains items like:
oneOf:
- float
- NumberWithValidation
- StringEnum
- ArrayModel
- null
TODO: lru_cache this
"""
if (
issubclass(cls, ModelSimple) or
cls in PRIMITIVE_TYPES
):
return True
elif issubclass(cls, ModelComposed):
if not cls._composed_schemas['oneOf']:
return False
return any(allows_single_value_input(c) for c in cls._composed_schemas['oneOf'])
return False
def composed_model_input_classes(cls):
"""
This function returns a list of the possible models that can be accepted as
inputs.
TODO: lru_cache this
"""
if issubclass(cls, ModelSimple) or cls in PRIMITIVE_TYPES:
return [cls]
elif issubclass(cls, ModelNormal):
if cls.discriminator is None:
return [cls]
else:
return get_discriminated_classes(cls)
elif issubclass(cls, ModelComposed):
if not cls._composed_schemas['oneOf']:
return []
if cls.discriminator is None:
input_classes = []
for c in cls._composed_schemas['oneOf']:
input_classes.extend(composed_model_input_classes(c))
return input_classes
else:
return get_discriminated_classes(cls)
return []
class OpenApiModel(object):
"""The base class for all OpenAPIModels"""
@@ -138,9 +190,17 @@ class OpenApiModel(object):
# pick a new schema/class to instantiate because a discriminator
# propertyName value was passed in
if len(args) == 1 and args[0] is None and is_type_nullable(cls):
# The input data is the 'null' value and the type is nullable.
return None
if len(args) == 1:
arg = args[0]
if arg is None and is_type_nullable(cls):
# The input data is the 'null' value and the type is nullable.
return None
if issubclass(cls, ModelComposed) and allows_single_value_input(cls):
model_kwargs = {}
oneof_instance = get_oneof_instance(cls, model_kwargs, kwargs, model_arg=arg)
return oneof_instance
visited_composed_classes = kwargs.get('_visited_composed_classes', ())
if (
@@ -508,6 +568,10 @@ UPCONVERSION_TYPE_PAIRS = (
(int, float), # A float may be serialized as an integer, e.g. '3' is a valid serialized float.
(list, ModelComposed),
(dict, ModelComposed),
(str, ModelComposed),
(int, ModelComposed),
(float, ModelComposed),
(list, ModelComposed),
(list, ModelNormal),
(dict, ModelNormal),
(str, ModelSimple),
@@ -886,20 +950,53 @@ def remove_uncoercible(required_types_classes, current_item, spec_property_namin
results_classes.append(required_type_class)
return results_classes
def get_discriminated_classes(cls):
"""
Returns all the classes that a discriminator converts to
TODO: lru_cache this
"""
possible_classes = []
key = list(cls.discriminator.keys())[0]
if is_type_nullable(cls):
possible_classes.append(cls)
for discr_cls in cls.discriminator[key].values():
if hasattr(discr_cls, 'discriminator') and discr_cls.discriminator is not None:
possible_classes.extend(get_discriminated_classes(discr_cls))
else:
possible_classes.append(discr_cls)
return possible_classes
def get_required_type_classes(required_types_mixed):
def get_possible_classes(cls, from_server_context):
# TODO: lru_cache this
possible_classes = [cls]
if from_server_context:
return possible_classes
if hasattr(cls, 'discriminator') and cls.discriminator is not None:
possible_classes = []
possible_classes.extend(get_discriminated_classes(cls))
elif issubclass(cls, ModelComposed):
possible_classes.extend(composed_model_input_classes(cls))
return possible_classes
def get_required_type_classes(required_types_mixed, spec_property_naming):
"""Converts the tuple required_types into a tuple and a dict described
below
Args:
required_types_mixed (tuple/list): will contain either classes or
instance of list or dict
spec_property_naming (bool): if True these values came from the
server, and we use the data types in our endpoints.
If False, we are client side and we need to include
oneOf and discriminator classes inside the data types in our endpoints
Returns:
(valid_classes, dict_valid_class_to_child_types_mixed):
valid_classes (tuple): the valid classes that the current item
should be
dict_valid_class_to_child_types_mixed (doct):
dict_valid_class_to_child_types_mixed (dict):
valid_class (class): this is the key
child_types_mixed (list/dict/tuple): describes the valid child
types
@@ -917,7 +1014,7 @@ def get_required_type_classes(required_types_mixed):
valid_classes.append(dict)
child_req_types_by_current_type[dict] = required_type[str]
else:
valid_classes.append(required_type)
valid_classes.extend(get_possible_classes(required_type, spec_property_naming))
return tuple(valid_classes), child_req_types_by_current_type
@@ -1070,7 +1167,7 @@ def deserialize_model(model_data, model_class, path_to_item, check_type,
"""Deserializes model_data to model instance.
Args:
model_data (list/dict): data to instantiate the model
model_data (int/str/float/bool/none_type/list/dict): data to instantiate the model
model_class (OpenApiModel): the model class
path_to_item (list): path to the model in the received data
check_type (bool): whether to check the data tupe for the values in
@@ -1096,14 +1193,14 @@ def deserialize_model(model_data, model_class, path_to_item, check_type,
_spec_property_naming=spec_property_naming)
if issubclass(model_class, ModelSimple):
instance = model_class(value=model_data, **kw_args)
return instance
if isinstance(model_data, list):
instance = model_class(*model_data, **kw_args)
return model_class(model_data, **kw_args)
elif isinstance(model_data, list):
return model_class(*model_data, **kw_args)
if isinstance(model_data, dict):
kw_args.update(model_data)
instance = model_class(**kw_args)
return instance
return model_class(**kw_args)
elif isinstance(model_data, PRIMITIVE_TYPES):
return model_class(model_data, **kw_args)
def deserialize_file(response_data, configuration, content_disposition=None):
@@ -1286,7 +1383,7 @@ def validate_and_convert_types(input_value, required_types_mixed, path_to_item,
Raises:
ApiTypeError
"""
results = get_required_type_classes(required_types_mixed)
results = get_required_type_classes(required_types_mixed, spec_property_naming)
valid_classes, child_req_types_by_current_type = results
input_class_simple = get_simple_class(input_value)
@@ -1523,7 +1620,7 @@ def get_allof_instances(self, model_args, constant_args):
return composed_instances
def get_oneof_instance(self, model_args, constant_args):
def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None):
"""
Find the oneOf schema that matches the input data (e.g. payload).
If exactly one schema matches the input data, an instance of that schema
@@ -1531,25 +1628,33 @@ def get_oneof_instance(self, model_args, constant_args):
If zero or more than one schema match the input data, an exception is raised.
In OAS 3.x, the payload MUST, by validation, match exactly one of the
schemas described by oneOf.
Args:
self: the class we are handling
model_args (dict): var_name to var_value
cls: the class we are handling
model_kwargs (dict): var_name to var_value
The input data, e.g. the payload that must match a oneOf schema
in the OpenAPI document.
constant_args (dict): var_name to var_value
constant_kwargs (dict): var_name to var_value
args that every model requires, including configuration, server
and path to item.
Kwargs:
model_arg: (int, float, bool, str, date, datetime, ModelSimple, None):
the value to assign to a primitive class or ModelSimple class
Notes:
- this is only passed in when oneOf includes types which are not object
- None is used to suppress handling of model_arg, nullable models are handled in __new__
Returns
oneof_instance (instance)
"""
if len(self._composed_schemas['oneOf']) == 0:
if len(cls._composed_schemas['oneOf']) == 0:
return None
oneof_instances = []
# Iterate over each oneOf schema and determine if the input data
# matches the oneOf schemas.
for oneof_class in self._composed_schemas['oneOf']:
for oneof_class in cls._composed_schemas['oneOf']:
# The composed oneOf schema allows the 'null' type and the input data
# is the null value. This is a OAS >= 3.1 feature.
if oneof_class is none_type:
@@ -1557,28 +1662,45 @@ def get_oneof_instance(self, model_args, constant_args):
# none_type deserialization is handled in the __new__ method
continue
# transform js keys from input data to python keys in fixed_model_args
fixed_model_args = change_keys_js_to_python(
model_args, oneof_class)
single_value_input = allows_single_value_input(oneof_class)
# Extract a dict with the properties that are declared in the oneOf schema.
# Undeclared properties (e.g. properties that are allowed because of the
# additionalProperties attribute in the OAS document) are not added to
# the dict.
kwargs = {}
var_names = set(oneof_class.openapi_types.keys())
for var_name in var_names:
if var_name in fixed_model_args:
kwargs[var_name] = fixed_model_args[var_name]
if not single_value_input:
# transform js keys from input data to python keys in fixed_model_args
fixed_model_args = change_keys_js_to_python(
model_kwargs, oneof_class)
# do not try to make a model with no input args
if len(kwargs) == 0:
continue
# Extract a dict with the properties that are declared in the oneOf schema.
# Undeclared properties (e.g. properties that are allowed because of the
# additionalProperties attribute in the OAS document) are not added to
# the dict.
kwargs = {}
var_names = set(oneof_class.openapi_types.keys())
for var_name in var_names:
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_kwargs)
# and use it to make the instance
kwargs.update(constant_args)
try:
oneof_instance = oneof_class(**kwargs)
if not single_value_input:
oneof_instance = oneof_class(**kwargs)
else:
if issubclass(oneof_class, ModelSimple):
oneof_instance = oneof_class(model_arg, **constant_kwargs)
elif oneof_class in PRIMITIVE_TYPES:
oneof_instance = validate_and_convert_types(
model_arg,
(oneof_class,),
constant_kwargs['_path_to_item'],
constant_kwargs['_spec_property_naming'],
constant_kwargs['_check_type'],
configuration=constant_kwargs['_configuration']
)
oneof_instances.append(oneof_instance)
except Exception:
pass
@@ -1586,13 +1708,13 @@ def get_oneof_instance(self, model_args, constant_args):
raise ApiValueError(
"Invalid inputs given to generate an instance of %s. None "
"of the oneOf schemas matched the input data." %
self.__class__.__name__
cls.__name__
)
elif len(oneof_instances) > 1:
raise ApiValueError(
"Invalid inputs given to generate an instance of %s. Multiple "
"oneOf schemas matched the inputs, but a max of one is allowed." %
self.__class__.__name__
cls.__name__
)
return oneof_instances[0]
@@ -1732,7 +1854,7 @@ def validate_get_composed_info(constant_args, model_args, self):
composed_instances = []
allof_instances = get_allof_instances(self, model_args, constant_args)
composed_instances.extend(allof_instances)
oneof_instance = get_oneof_instance(self, model_args, constant_args)
oneof_instance = get_oneof_instance(self.__class__, model_args, constant_args)
if oneof_instance is not None:
composed_instances.append(oneof_instance)
anyof_instances = get_anyof_instances(self, model_args, constant_args)