forked from loafle/openapi-generator-original
[python] Fixes additional_properties_type for models (#8802)
* Fixes additionalProperties values for models, updates docs, adds tag test of it, fixes frit and gmfruit tests * Moves this.setDisallowAdditionalPropertiesIfNotPresent higher * Makes setting additional_properties_model_instances contingent on the presence of addprosp in schema, updates sample spec composed schemas to remove addprops False form two * Fixes oneOf anyOf allOf instantiation logic * Removes Address from Cat definition * Adds required vars for apple and banana, removes required vars from composed schema init sig * Updates composed schema vars to be set on self and all composed instances * Removes get_unused_args, get_var_name_to_model_instances, and get_additional_properties_model_instances * Fixes fruit + deserilization tests, creates ComposedSchemaWithPropsAndNoAddProps * Fixes FruitReq tests * Fixes GmFruit tests * Fixes discard_unknown_keys tests * Samples updated * Removes additionalproperties False in Child * Samples updated * Improves handling of v2 and v3 specs for isFreeFormObject, v2 sample spec updated with link to bug * Adds cli option disallowAdditionalPropertiesIfNotPresent to python * Adds getAdditionalProperties method so the value for addProps will be correct * Reverts file * Reverts file * Updates python doc * Reverted anytype_3 definition * Updates test_deserialize_lizard * Updates test_deserialize_dict_str_dog * Updates testDog * Updates testChild * Adds v2 python_composition sample * Adds needed files for python testing * Adds existing tests into the new python sample * Fixes test_dog * Removes addProps false form Dog * Fixes testChild * Updates how additionalProperties are set * Fixes empty_map type * Type generation fixed for v2 and v3 specs * Refactors getTypeString, updates artifactids in pom.xml files * Adds new python sample to CI testing I think * Fixes artifactId collision, regenrates docs
This commit is contained in:
@@ -434,27 +434,43 @@ class ModelComposed(OpenApiModel):
|
||||
self.__dict__[name] = value
|
||||
return
|
||||
|
||||
# set the attribute on the correct instance
|
||||
model_instances = self._var_name_to_model_instances.get(
|
||||
name, self._additional_properties_model_instances)
|
||||
if model_instances:
|
||||
for model_instance in model_instances:
|
||||
if model_instance == self:
|
||||
self.set_attribute(name, value)
|
||||
else:
|
||||
setattr(model_instance, name, value)
|
||||
if name not in self._var_name_to_model_instances:
|
||||
# we assigned an additional property
|
||||
self.__dict__['_var_name_to_model_instances'][name] = (
|
||||
model_instance
|
||||
)
|
||||
return None
|
||||
"""
|
||||
Use cases:
|
||||
1. additional_properties_type is None (additionalProperties == False in spec)
|
||||
Check for property presence in self.openapi_types
|
||||
if not present then throw an error
|
||||
if present set in self, set attribute
|
||||
always set on composed schemas
|
||||
2. additional_properties_type exists
|
||||
set attribute on self
|
||||
always set on composed schemas
|
||||
"""
|
||||
if self.additional_properties_type is None:
|
||||
"""
|
||||
For an attribute to exist on a composed schema it must:
|
||||
- fulfill schema_requirements in the self composed schema not considering oneOf/anyOf/allOf schemas AND
|
||||
- fulfill schema_requirements in each oneOf/anyOf/allOf schemas
|
||||
|
||||
raise ApiAttributeError(
|
||||
"{0} has no attribute '{1}'".format(
|
||||
type(self).__name__, name),
|
||||
[e for e in [self._path_to_item, name] if e]
|
||||
)
|
||||
schema_requirements:
|
||||
For an attribute to exist on a schema it must:
|
||||
- be present in properties at the schema OR
|
||||
- have additionalProperties unset (defaults additionalProperties = any type) OR
|
||||
- have additionalProperties set
|
||||
"""
|
||||
if name not in self.openapi_types:
|
||||
raise ApiAttributeError(
|
||||
"{0} has no attribute '{1}'".format(
|
||||
type(self).__name__, name),
|
||||
[e for e in [self._path_to_item, name] if e]
|
||||
)
|
||||
# attribute must be set on self and composed instances
|
||||
self.set_attribute(name, value)
|
||||
for model_instance in self._composed_instances:
|
||||
setattr(model_instance, name, value)
|
||||
if name not in self._var_name_to_model_instances:
|
||||
# we assigned an additional property
|
||||
self.__dict__['_var_name_to_model_instances'][name] = self._composed_instances + [self]
|
||||
return None
|
||||
|
||||
__unset_attribute_value__ = object()
|
||||
|
||||
@@ -464,13 +480,12 @@ class ModelComposed(OpenApiModel):
|
||||
return self.__dict__[name]
|
||||
|
||||
# get the attribute from the correct instance
|
||||
model_instances = self._var_name_to_model_instances.get(
|
||||
name, self._additional_properties_model_instances)
|
||||
model_instances = self._var_name_to_model_instances.get(name)
|
||||
values = []
|
||||
# A composed model stores child (oneof/anyOf/allOf) models under
|
||||
# self._var_name_to_model_instances. A named property can exist in
|
||||
# multiple child models. If the property is present in more than one
|
||||
# child model, the value must be the same across all the child models.
|
||||
# A composed model stores self and child (oneof/anyOf/allOf) models under
|
||||
# self._var_name_to_model_instances.
|
||||
# Any property must exist in self and all model instances
|
||||
# The value stored in all model instances must be the same
|
||||
if model_instances:
|
||||
for model_instance in model_instances:
|
||||
if name in model_instance._data_store:
|
||||
@@ -1573,8 +1588,13 @@ def get_allof_instances(self, model_args, constant_args):
|
||||
self: the class we are handling
|
||||
model_args (dict): var_name to var_value
|
||||
used to make instances
|
||||
constant_args (dict): var_name to var_value
|
||||
used to make instances
|
||||
constant_args (dict):
|
||||
metadata arguments:
|
||||
_check_type
|
||||
_path_to_item
|
||||
_spec_property_naming
|
||||
_configuration
|
||||
_visited_composed_classes
|
||||
|
||||
Returns
|
||||
composed_instances (list)
|
||||
@@ -1582,20 +1602,8 @@ def get_allof_instances(self, model_args, constant_args):
|
||||
composed_instances = []
|
||||
for allof_class in self._composed_schemas['allOf']:
|
||||
|
||||
# no need to handle changing js keys to python because
|
||||
# for composed schemas, allof parameters are included in the
|
||||
# composed schema and were changed to python keys in __new__
|
||||
# extract a dict of only required keys from fixed_model_args
|
||||
kwargs = {}
|
||||
var_names = set(allof_class.openapi_types.keys())
|
||||
for var_name in var_names:
|
||||
if var_name in model_args:
|
||||
kwargs[var_name] = model_args[var_name]
|
||||
|
||||
# and use it to make the instance
|
||||
kwargs.update(constant_args)
|
||||
try:
|
||||
allof_instance = allof_class(**kwargs)
|
||||
allof_instance = allof_class(**model_args, **constant_args)
|
||||
composed_instances.append(allof_instance)
|
||||
except Exception as ex:
|
||||
raise ApiValueError(
|
||||
@@ -1655,31 +1663,9 @@ def get_oneof_instance(cls, model_kwargs, constant_kwargs, model_arg=None):
|
||||
|
||||
single_value_input = allows_single_value_input(oneof_class)
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
try:
|
||||
if not single_value_input:
|
||||
oneof_instance = oneof_class(**kwargs)
|
||||
oneof_instance = oneof_class(**model_kwargs, **constant_kwargs)
|
||||
else:
|
||||
if issubclass(oneof_class, ModelSimple):
|
||||
oneof_instance = oneof_class(model_arg, **constant_kwargs)
|
||||
@@ -1736,24 +1722,8 @@ def get_anyof_instances(self, model_args, constant_args):
|
||||
# none_type deserialization is handled in the __new__ method
|
||||
continue
|
||||
|
||||
# transform js keys to python keys in fixed_model_args
|
||||
fixed_model_args = change_keys_js_to_python(model_args, anyof_class)
|
||||
|
||||
# extract a dict of only required keys from these_model_vars
|
||||
kwargs = {}
|
||||
var_names = set(anyof_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_args)
|
||||
try:
|
||||
anyof_instance = anyof_class(**kwargs)
|
||||
anyof_instance = anyof_class(**model_args, **constant_args)
|
||||
anyof_instances.append(anyof_instance)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1766,47 +1736,34 @@ def get_anyof_instances(self, model_args, constant_args):
|
||||
return anyof_instances
|
||||
|
||||
|
||||
def get_additional_properties_model_instances(
|
||||
composed_instances, self):
|
||||
additional_properties_model_instances = []
|
||||
all_instances = [self]
|
||||
all_instances.extend(composed_instances)
|
||||
for instance in all_instances:
|
||||
if instance.additional_properties_type is not None:
|
||||
additional_properties_model_instances.append(instance)
|
||||
return additional_properties_model_instances
|
||||
|
||||
|
||||
def get_var_name_to_model_instances(self, composed_instances):
|
||||
var_name_to_model_instances = {}
|
||||
all_instances = [self]
|
||||
all_instances.extend(composed_instances)
|
||||
for instance in all_instances:
|
||||
for var_name in instance.openapi_types:
|
||||
if var_name not in var_name_to_model_instances:
|
||||
var_name_to_model_instances[var_name] = [instance]
|
||||
else:
|
||||
var_name_to_model_instances[var_name].append(instance)
|
||||
return var_name_to_model_instances
|
||||
|
||||
|
||||
def get_unused_args(self, composed_instances, model_args):
|
||||
unused_args = dict(model_args)
|
||||
# arguments apssed to self were already converted to python names
|
||||
def get_discarded_args(self, composed_instances, model_args):
|
||||
"""
|
||||
Gathers the args that were discarded by configuration.discard_unknown_keys
|
||||
"""
|
||||
model_arg_keys = model_args.keys()
|
||||
discarded_args = set()
|
||||
# arguments passed to self were already converted to python names
|
||||
# before __init__ was called
|
||||
for var_name_py in self.attribute_map:
|
||||
if var_name_py in unused_args:
|
||||
del unused_args[var_name_py]
|
||||
for instance in composed_instances:
|
||||
if instance.__class__ in self._composed_schemas['allOf']:
|
||||
for var_name_py in instance.attribute_map:
|
||||
if var_name_py in unused_args:
|
||||
del unused_args[var_name_py]
|
||||
try:
|
||||
keys = instance.to_dict().keys()
|
||||
discarded_keys = model_args - keys
|
||||
discarded_args.update(discarded_keys)
|
||||
except Exception:
|
||||
# allOf integer schema will throw exception
|
||||
pass
|
||||
else:
|
||||
for var_name_js in instance.attribute_map.values():
|
||||
if var_name_js in unused_args:
|
||||
del unused_args[var_name_js]
|
||||
return unused_args
|
||||
try:
|
||||
all_keys = set(model_to_dict(instance, serialize=False).keys())
|
||||
js_keys = model_to_dict(instance, serialize=True).keys()
|
||||
all_keys.update(js_keys)
|
||||
discarded_keys = model_arg_keys - all_keys
|
||||
discarded_args.update(discarded_keys)
|
||||
except Exception:
|
||||
# allOf integer schema will throw exception
|
||||
pass
|
||||
return discarded_args
|
||||
|
||||
|
||||
def validate_get_composed_info(constant_args, model_args, self):
|
||||
@@ -1850,36 +1807,42 @@ def validate_get_composed_info(constant_args, model_args, self):
|
||||
composed_instances.append(oneof_instance)
|
||||
anyof_instances = get_anyof_instances(self, model_args, constant_args)
|
||||
composed_instances.extend(anyof_instances)
|
||||
"""
|
||||
set additional_properties_model_instances
|
||||
additional properties must be evaluated at the schema level
|
||||
so self's additional properties are most important
|
||||
If self is a composed schema with:
|
||||
- no properties defined in self
|
||||
- additionalProperties: False
|
||||
Then for object payloads every property is an additional property
|
||||
and they are not allowed, so only empty dict is allowed
|
||||
|
||||
Properties must be set on all matching schemas
|
||||
so when a property is assigned toa composed instance, it must be set on all
|
||||
composed instances regardless of additionalProperties presence
|
||||
keeping it to prevent breaking changes in v5.0.1
|
||||
TODO remove cls._additional_properties_model_instances in 6.0.0
|
||||
"""
|
||||
additional_properties_model_instances = []
|
||||
if self.additional_properties_type is not None:
|
||||
additional_properties_model_instances = [self]
|
||||
|
||||
"""
|
||||
no need to set properties on self in here, they will be set in __init__
|
||||
By here all composed schema oneOf/anyOf/allOf instances have their properties set using
|
||||
model_args
|
||||
"""
|
||||
discarded_args = get_discarded_args(self, composed_instances, model_args)
|
||||
|
||||
# map variable names to composed_instances
|
||||
var_name_to_model_instances = get_var_name_to_model_instances(
|
||||
self, composed_instances)
|
||||
|
||||
# set additional_properties_model_instances
|
||||
additional_properties_model_instances = (
|
||||
get_additional_properties_model_instances(composed_instances, self)
|
||||
)
|
||||
|
||||
# set any remaining values
|
||||
unused_args = get_unused_args(self, composed_instances, model_args)
|
||||
if len(unused_args) > 0 and \
|
||||
len(additional_properties_model_instances) == 0 and \
|
||||
(self._configuration is None or
|
||||
not self._configuration.discard_unknown_keys):
|
||||
raise ApiValueError(
|
||||
"Invalid input arguments input when making an instance of "
|
||||
"class %s. Not all inputs were used. The unused input data "
|
||||
"is %s" % (self.__class__.__name__, unused_args)
|
||||
)
|
||||
|
||||
# no need to add additional_properties to var_name_to_model_instances here
|
||||
# because additional_properties_model_instances will direct us to that
|
||||
# instance when we use getattr or setattr
|
||||
# and we update var_name_to_model_instances in setattr
|
||||
var_name_to_model_instances = {}
|
||||
for prop_name in model_args:
|
||||
if prop_name not in discarded_args:
|
||||
var_name_to_model_instances[prop_name] = [self] + composed_instances
|
||||
|
||||
return [
|
||||
composed_instances,
|
||||
var_name_to_model_instances,
|
||||
additional_properties_model_instances,
|
||||
unused_args
|
||||
discarded_args
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user