-
-
Notifications
You must be signed in to change notification settings - Fork 228
bugfix: correct indentation of additionalProperties that are union types #266 #268
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
Conversation
check for list not List in isinstance cast primitives returned by union property's _parse_*
cc @dbanty |
Codecov Report
@@ Coverage Diff @@
## main #268 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 46 46
Lines 1314 1325 +11
=========================================
+ Hits 1314 1325 +11
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.
Thanks for fixing this so quickly! There are a couple oddities around the Unions which aren't caused by this but I noticed here that I think we can clean up.
If you don't have the bandwidth to look into that cleanup in the near term, we can always open a separate issue to clean up in a future version so we can get this bug fix out, just let me know.
return cast(str, prop_dict) | ||
return cast(float, prop_dict) | ||
return cast(int, prop_dict) | ||
return cast(bool, prop_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.
Hmm, this is ugly. I'll see if I can find the cause and propose a solution further down.
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 would do this before, just without the cast
s. It's because in union_property.pyi
we loop through each property and if it doesn't have a template we just return it.
What is here is ugly, yes, but it works. However, I do think there is an issue if we declare a templated type before a primitive type - then the primitive type would cause it to return early. Something , like
try:
additional_property = cast(List[str], data)
return additional_property
except: # noqa: E722
pass
return cast(str, prop_dict)
try:
additional_property = ModelWithAnyJsonPropertiesAdditionalProperty.from_dict(data)
return additional_property
except: # noqa: E722
pass
return cast(float, data)
return cast(int, data)
return cast(bool, data)
So I think we need to loop over templated properties first, then untemplated properties. Also we'll want to figure out lists, as casting it to a List
will not error so that breaks the try/catch logic.
While responding to this comment, I realized that these should be using cast(<primitive type>, data)
not cast(<primitive type>, prop_dict)
- I added commits to this branch to change that
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'll try looking at this soon as I have time
elif isinstance(prop, list): | ||
field_dict[prop_name] = prop | ||
|
||
elif isinstance(prop, str): | ||
field_dict[prop_name] = prop | ||
elif isinstance(prop, float): | ||
field_dict[prop_name] = prop | ||
elif isinstance(prop, int): | ||
field_dict[prop_name] = prop | ||
else: | ||
field_dict[prop_name] = prop |
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.
Feels like maybe we need a way to indicate whether or not a given property actually needs to be transformed and if not, just have an else
. Then something similar with construct for from_dict
.
{% else %} | ||
return {{ source }} | ||
return cast({{ inner_property.get_type_string() }}, {{ source }}) | ||
{% 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.
So if we aren't running a construct macro because the inner prop doesn't have a template, then we really aren't doing anything with it. So instead of having this in a loop, we could just do this after the loop:
return cast({{ property.get_type_string() }}, data)
So basically, loop through all the things that require some construction, if none of those work, just return what we have and assume it's one of the values. Technically it's one of the values we didn't check yet, but that doesn't really matter to the return type since the calling function doesn't know which union member was selected.
Also, it doesn't look like we actually need to use source
anymore since the param is data
right? So we could pull that out of the macro.
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.
We do still need source
because it's used in the outer function to call this inner one (just below on line 22, right before the endmacro
)
{% for inner_property in property.inner_properties %} | ||
{% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #} | ||
if isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): | ||
if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): | ||
{% elif not loop.last %} | ||
elif isinstance({{ source }}, {{ inner_property.get_type_string(no_optional=True) }}): | ||
elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}): | ||
{% else %} | ||
else: | ||
{% 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.
Similar story here I think, we just change the order of some checks. Something like
{% for inner_property in (inner_property for inner_property in property.inner_properties if inner_property.template) %}
{% if loop.first and property.required and not property.nullable %}{# No if UNSET or if None statement before this #}
if isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}):
{% else %}
elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}):
{% from "property_templates/" + inner_property.template import transform %}
{{ transform(inner_property, source, destination, declare_type=False) | indent(4) }}
{% endif %}
{% endfor %}
else:
{{ destination }} = {{ source }}
Editing in GH UI, so probably some mistakes made, but hopefully you get the idea.
Ok, I've updated this by adding I thought for a moment that I would need to handle As an aside - I'm off until 1/4 and not doing any dev work while I'm off, so I won't be checking in to follow up here until then. Feel free to take this and/or expand on it without me. |
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.
Looks good, thanks again. I had one note about how the properties without templates are being used, I'll check this out and fix it so we can get this merged in.
Enjoy your time off!
{% for inner_property in property.inner_properties %} | ||
{% if inner_property.template and not loop.last %} | ||
{% for inner_property in property.inner_properties_with_template %} | ||
{% if not loop.last or property.inner_properties_without_template %} |
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 will loop through all the properties for each property with templates to recompute inner_properties_without_template
. I generally prefer leaving methods that do work as methods instead of using @property
to make it more obvious that "this does something".
I'll check this out and make a tweak to avoid any O(n^2) behavior which, while probably insignificant in most cases, I'd still like to avoid if possible.
- Added CHANGELOG entry - Change inner_properties_without_template to bool generated once - Change inner_properties_with_template to generator
Closing in favor of #273 which is based on this but has a couple tweaks:
|
Fixes #266
Primary fix was to
model.pyi
, passing in a correct value toindent
when looping through additional propertiesAdded
ModelWithAnyJsonProperties
model toopenapi.json
for e2e testingWith the new test model, encountered a couple typing issues and fixed them:
to_dict
for union properties it usesisinstance
to inspect the type, but in the case of lists where it would checkisinstance(prop, List[str])
get_instance_type_string
toProperty
and using thatListProperty
s are the only props with a special caseget_instance_type_string
from_dict
for unioned properties where one of the options is a primitive, it would just return the value to the helper func, which is provided asAny
and thus would complain about the typecast
ing the primitivemodel_with_any_json_properties.py
, the but it should still work as the types from this helper func internal to thefrom_dict
function