-
-
Notifications
You must be signed in to change notification settings - Fork 6.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: handle deepcopy of openapi objects #9735
Conversation
merges code from upstream PR: OpenAPITools#9735
Closing and re-opening for travis |
...nerator/src/main/resources/python/model_templates/method_from_openapi_data_composed.mustache
Outdated
Show resolved
Hide resolved
def __copy__(self): | ||
cls = self.__class__ | ||
result = cls.__new__(cls) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this copying a payload instantiated from client data cls(**kwargs)
option1
Or one received from the server cls._from_openapi_data(**kwargs)
option2?
If so should we not use __new__
and use one of the above options?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that seems reasonble, ideally just _from_openapidata should be used, since we are copying the "data", and don't want the type checking (ie. checking for read-only would not be required).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So how about changing this to use _from_openapi_data rather than __new__
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree, i will make that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: one can use the self._spec_property_naming to determine which code path to use when copying a self instance.
@@ -224,7 +225,8 @@ class OpenApiModel(object): | |||
self_inst.__init__(*args, **kwargs) | |||
|
|||
new_inst = new_cls.__new__(new_cls, *args, **kwargs) | |||
new_inst.__init__(*args, **kwargs) | |||
# use _from_openapi_data to handle readOnly discriminator | |||
new_inst = new_inst._from_openapi_data(*args, **kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once we get rid of the new invocation in the copy method can this code be reverted?
If not then do we need to be two code paths here?
- For from_openapi_data == True, invoke _from_openapi_data
- For from_openapi_data == False invoke new then init
This same comment applies to the get_allof_instances update also
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its the latter, its still needed (the openapi_data check). Agree, it would be more accurate to selectively call the correct init call. I will make that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is inside a new, trying to figure out if this is called from init or from_openapi_data is tricky since new only has a single arg (the class name).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems the mechansim of validating the read-only vars at construction time (via init vs. from_openapi_data) leaves this code in a bit of a bind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use kwargs['_spec_property_naming'] to decide which code path to use?
I think that
- _spec_property_naming = True is for the from_server use case / which is the from_openapi_data use case
- _spec_property_naming = False is the client side instantiation use case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our code sets it to True
here:
https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/resources/python/api_client.mustache#L340
when values are received from the server
Also we pas through _spec_propertty_naming in constant_args so can you revert your updates that add from_openapi_data as an argument and instead use _spec_propertty_naming?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my concern is, this spec_property_naming is set via the deserialize call.
If I instantiate an object with _from_openapi_data(), and then copy that, I think that means now that I have to manually set this parameter (_spec_property_naming).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm that's true. Should all _from_openapi_data methods hard code _spec_property_naming = True?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
possibly, though I'm not sure. My understanding is, _from_openapi_data is similiar to init, so vars are "pythonic". So that means it shouldn't be _spec_property_naming. I believe this tells me that we can't use_spec_property_naming for the purpose that I would like.
I don't quite appreciate the purpose of the _from vs. init. Couldn't the same thing be achieved and keep a single constructor initializer and just an option for checking read-only? I guess that is another discussion separate from this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_from_openapi data is used to instantiate models with data received from the server. When making model instances with readonly parameters the fn signature needed to make a model instances must contain different parameters if the data came from the server vs from the client. You can read more about it here: #9296 (comment). That conversation is separate from this one.
_from_openapi_data is used when instantiating models from the server so I'm pretty sure that we could depend on _spec_property_naming being true in it. We could throw an ApiValueError if _spec_property_naming is the arguments in _from_openapi_data.
Hi @spacether PR updated and rebased with changes as per review comments. Thanks. |
@@ -186,6 +187,24 @@ def __getattr__(self, attr): | |||
"""get the value of an attribute using dot notation: `instance.attr`""" | |||
return self.__getitem__(attr) | |||
|
|||
def __copy__(self): | |||
cls = self.__class__ | |||
return cls._new_from_openapi_data(**self.__dict__) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is copy assuming a from server context? Why not use self._spec_property_naming to pick if we should use __new__
or _new_from_openapi_data
?
This same question applies to __deepcopy__
, why is it assuming a from server context when setting _spec_property_naming to True?
What if the deepcopy instance was made client side, not server side, then this code incorrectly forces _spec_property_naming to True when it should be False.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I have an object, I would assume that should be able to copy it as is, without consideration for read-only args/etc. Now I can't think of a case where that would occur, but I'm not sure I have a grasp of all the ways objects can be created at moment. That was my reasoning, but if you still feel the logic you have described should be used, I will update to that. thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You will be able to copy an instance of the model that is made using client side data, and you can do the same with an instance of the model made with data from the server, but those contexts should allow slightly different data to be stored in the instances. Changing _spec_property_naming changes where the model says its data came from, which we should not be doing.
Please make the suggested changes, thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have updated based on your recommendation.
A question regarding the the petstore_api tests, how exactly are those run? I typically run those manually, but I don't see a CI job that actually runs those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They were run by travis, I need to switch them over to CircleCi because we hit a free limit usage on Travis.
Merging this is blocked by that fix.
Thank you for making the changes.
.../client/petstore/python_disallowAdditionalPropertiesIfNotPresent/petstore_api/model_utils.py
Outdated
Show resolved
Hide resolved
new_inst = cls.__new__(cls, **kwargs) | ||
|
||
for k, v in self.__dict__.items(): | ||
setattr(new_inst, k, deepcopy(v, memo)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about grabbing all of these properties before passing them into
_new_from_openapi_data
and __new__
?
kwargs = {}
for k, v in self.__dict__.items():
kwargs[key] = deepcopy(v, memo)
if self.get("_spec_property_naming", False):
new_inst = cls._new_from_openapi_data(**kwargs)
else:
new_inst = cls.__new__(cls, **kwargs)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason setattr is used rather than kwargs, is that the dictionary set of values (such as the properties), are stored in dict["_data_store"]. In order to pass as kwargs, I would need to interpret the data (handle the _data_store as a special case), and pass that in as the kwargs. Rather, using setinst sets the key/value as is without any interpretation, a more resilent solution IMO.
modules/openapi-generator/src/main/resources/python/model_templates/methods_shared.mustache
Show resolved
Hide resolved
modules/openapi-generator/src/main/resources/python/model_templates/methods_shared.mustache
Outdated
Show resolved
Hide resolved
- Add __deepcopy__ and __copy__ to OpenApiModel - pass discriminator inside deepcopy if exists - add test cases for deepcopy of models
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for rebasing; this looks great!
PR checklist
This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
These must match the expectations made by your contribution.
You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example
./bin/generate-samples.sh bin/configs/java*
.For Windows users, please run the script in Git BASH.
master
,5.1.x
,6.0.x
@taxpon (2017/07) @frol (2017/07) @mbohlool (2017/07) @cbornet (2017/09) @kenjones-cisco (2017/11) @tomplus (2018/10) @Jyhess (2019/01) @arun-nalla (2019/11) @spacether (2019/11)