-
-
Notifications
You must be signed in to change notification settings - Fork 228
Add additional property support by subscripting model #252
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
Add additional property support by subscripting model #252
Conversation
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.
Overall I think it looks good. I'm a little uneasy about putting set/getitem directly on the model but I don't have a good reason for being uneasy so it's probably fine 😅. I pointed out a couple places I think we could clean up some of the dict changes. Other than that, I would like @emannguitar to take a look at this as well to get his opinion.
{% for property in model.required_properties + model.optional_properties %} | ||
{% if property.required %} | ||
"{{ property.name }}": {{ property.python_name }}, | ||
{% endif %} | ||
{% endfor %} | ||
} | ||
}) |
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 not leave the regular properties in the field dict declaration? Would be a bit faster I think rather than having to update after the fact, even without having additional properties.
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 thinking here was that if a user for some reason set the same property both ways, the modeled version of the property takes precedence
model.some_json_property = "val1"
model["someJsonProperty"] = "val2"
model.to_dict()
# {"someJsonProperty": "val1"}
{% for property in model.required_properties + model.optional_properties %} | ||
{{ property.python_name }}={{ property.python_name }}, | ||
{% endfor %} | ||
) | ||
|
||
{% if model.additional_properties %} | ||
{{model.reference.module_name}}._additional_properties.update(d) |
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 update vs set directly? I would think assignment would be faster but maybe not.
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.
For some reason I had it in my head that attr
wouldn't like this. But these models aren't frozen so I'll make that change.
|
||
@property | ||
def additional_properties(self) -> Dict[str, {{ additional_property_type }}]: | ||
return self._additional_properties |
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.
What's the benefit of having this internal w/ @property
vs just declaring it as additional_properties
originally?
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.
There isn't one really. I had thought I might need to add some special handling here but ended up not needing it.
I've gone ahead and just made additional_properties
public.
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.
LGTM outside of @dbanty's comments
Actually two small things:
|
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 the feedback @dbanty and @emannguitar.
I realized I also didn't properly handle serialization/deserialization when the schema of the additionalProperties
is specified and the dict holds models. I will take a crack at that.
- Per the OpenAPI spec, type: object by itself (i.e. without specifying additionalProperties: true or additionalProperties: {}) is also a free-form object
This wasn't immediately clear to me. I know this is the case for JSON schema, wasn't sure it was for OpenAPI, but it appears so: "Consistent with JSON Schema, additionalProperties
defaults to true."
I will revisit to account for this.
- Given that a free-form object is really just an arbitrary dict at the end of the day, they should just be dicts instead of a model with nothing but _aditional_properties
This is true and was my initial thought for objects with additionalProperties
and no named properties
, but it gets more complicated when we get to the case I mentioned at the top of this comment and it's a Dict[str, SomeModelClass]
{% for property in model.required_properties + model.optional_properties %} | ||
{% if property.required %} | ||
"{{ property.name }}": {{ property.python_name }}, | ||
{% endif %} | ||
{% endfor %} | ||
} | ||
}) |
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 thinking here was that if a user for some reason set the same property both ways, the modeled version of the property takes precedence
model.some_json_property = "val1"
model["someJsonProperty"] = "val2"
model.to_dict()
# {"someJsonProperty": "val1"}
{% for property in model.required_properties + model.optional_properties %} | ||
{{ property.python_name }}={{ property.python_name }}, | ||
{% endfor %} | ||
) | ||
|
||
{% if model.additional_properties %} | ||
{{model.reference.module_name}}._additional_properties.update(d) |
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.
For some reason I had it in my head that attr
wouldn't like this. But these models aren't frozen so I'll make that change.
|
||
@property | ||
def additional_properties(self) -> Dict[str, {{ additional_property_type }}]: | ||
return self._additional_properties |
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.
There isn't one really. I had thought I might need to add some special handling here but ended up not needing it.
I've gone ahead and just made additional_properties
public.
…g models, test edge case parsing
OK, I've now updated this to properly interpret I also added support for I'll start working on tests soon, but again wanted to get this out there. |
{% if initial_value != None %} | ||
{{ property.python_name }} = {{ initial_value }} | ||
if {{ source }} is not None: | ||
{{ property.python_name }} = {{ property.reference.class_name }}.from_dict(cast(Dict[str, Any], {{ source }})) | ||
{% elif property.nullable %} | ||
{{ property.python_name }} = None | ||
{% else %} | ||
{{ property.python_name }} = UNSET | ||
{% endif %} |
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.
This addresses what I think was a bug before
(Below has to do with the fact we are now pop
ing the dict values)
@@ -16,7 +23,7 @@ if {{ source }} is not None: | |||
{{ destination }} = {{ source }}.to_dict() | |||
{% endif %} | |||
{% else %} | |||
{{ destination }}{% if declare_type %}: {{ property.get_type_string() }}{% endif %} = UNSET | |||
{{ destination }}{% if declare_type %}: Union[{% if property.nullable %}None, {% endif %}Unset, Dict[str, Any]]{% endif %} = UNSET |
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.
Same here - the type was wrong here
Codecov Report
@@ Coverage Diff @@
## main #252 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 46 46
Lines 1302 1314 +12
=========================================
+ Hits 1302 1314 +12
Continue to review full report at Codecov.
|
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.
LGTM!
@dbanty I generated a client for inventory to test and this seems to fix the issues we were having
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.
Awesome, fantastic work! I'll put together a release including this feature shortly.
Implements #218
Add support for
additionalProperties
by holding extra props in a private dict.Can be accessed with pass-through subscripting dunder methods, including a
@property
to get the keys (see the bottom ofmodel.pyi
)from_dict
topop
all named properties (from a shallow copy), with the rest going in the catch-allto_dict
sets the additional properties first, giving precedent to named propsThis would be a big win for my team - we had been relying on the previous behavior of having dicts when
type=object
to handle cases that we haven't or can't model very well.