-
-
Notifications
You must be signed in to change notification settings - Fork 227
Better compatibility for "required" vs. "nullable" #230
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
Better compatibility for "required" vs. "nullable" #230
Conversation
…uired and nullable. Model to_dict methods now have parameters to alter what fields are included/excluded
Codecov Report
@@ Coverage Diff @@
## main #230 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 41 41
Lines 1307 1332 +25
=========================================
+ Hits 1307 1332 +25
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.
A couple things:
- I think we should reduce the complexity of model
to_dict
back down unless there's a good reason for it to be so configurable. - I think we need to handle the type
Unset
everywhere it's usable, otherwise we're lying about the type which could lead to exceptions from client consumers.
date_prop: datetime.date = isoparse("1010-10-10").date(), | ||
float_prop: float = 3.14, | ||
int_prop: int = 7, | ||
boolean_prop: bool = cast(bool, 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.
I think the right thing to do with these would be to make the type Union[bool, Unset]
when the prop is not required. That way we're not lying about the data that could be in an instance of this class.
|
||
import attr | ||
|
||
Unset = NewType("Unset", object) | ||
UNSET: Any = Unset(object()) |
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 the type of this not Unset
? I haven't actually used NewType
before.
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 would say if we can't use Unset
as an actual type both here and when typing potential UNSET
s, we should class Unset:
to make it possible.
include: Optional[Set[str]] = None, | ||
exclude: Optional[Set[str]] = None, | ||
exclude_unset: bool = False, | ||
exclude_none: bool = 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.
A couple notes here:
- Are these here just in case someone wants them? Or was there a request specifically for this? I'd rather not add stuff to the API unless there's a demand for it since it slows things down a bit and might mean we have to breaking change it in the future.
- When would we ever want to not
exclude_unset
?
My instinct is to scratch these params and always skip values that are 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.
The need originally spawned out of #228, but realistically theres no need atm as the separation between required
and nullable
fixes the issue I was trying to address
Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com>
|
||
if self.nested_list_of_enums is UNSET: | ||
nested_list_of_enums = UNSET | ||
else: | ||
nested_list_of_enums = [] | ||
for nested_list_of_enums_item_data in self.nested_list_of_enums: |
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.
@dbanty I'm having a lot of trouble with blocks like this - mypy doesn't like "reassigning" in the else:
Incompatible types in assignment (expression has type "List[<nothing>]", variable has type "Unset")
What I could do is add a nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET
before the if statement and change the if/else to just be if self.nested_list_of_enums is not None
Mypy also complains about line 44 as it thinks the type of self.nested_list_of_enums
can still be Unset
. Changing line 40 to be if isinstance(self.nested_list_of_enums, Unset)
fixes this issue, giving way to a new pattern combining both:
nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]] = UNSET
if not isinstance(self.nested_list_of_enums, Unset):
nested_list_of_enums = []
etc.
The only problem is this gets pretty icky when trying to implement in the templates + account for stuff that can be None
. Any advice on how to do this in a way thats less icky to implement?
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 the if/else thing, you can declare the type before the block like:
nested_list_of_enums: Union[Unset, List[List[DifferentEnum]]]
if self.nested_list_of_enums is UNSET:
nested_list_of_enums = UNSET
else:
nested_list_of_enums = []
I believe mypy will be fine with that, though I'm not sure it's much better than what you had. Unfortunately I think is instance
is the only way to handle the branches, because we told mypy it can be any Unset
, not just UNSET
. I don't think you can use Literal with an object...
Maybe we could do what Pydantic does and use ellipsis ...
for our unset placeholder?
thing: Union[int, Literal[...]] = ...
if thing is ...:
...
else:
...
I don't know how you or mypy would feel about that. We could also alias UNSET = Literal[...]
for clearer typing.
@dbanty Ready for another review pass - unfortunately mypy didn't like |
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.
A couple nit pickles, mostly related to combining no_optional and no_unset because I think we don't need both. If you disagree with me that's fine, just leave them as is.
Also please merge main into this
json_list_prop = [] | ||
for list_prop_item_data in list_prop: | ||
list_prop_item = list_prop_item_data.value | ||
|
||
json_list_prop.append(list_prop_item) | ||
|
||
if union_prop is None: | ||
json_union_prop: Optional[Union[Optional[float], Optional[str]]] = None | ||
json_union_prop: Union[Unset, Union[float, str]] |
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.
Would be nice to handle nested unions better. I wonder if there's a function in typing somewhere that can simplify types like this. Not necessary for this PR but would be a good future enhancement.
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 think manually adding Unset to the union should work fine, I'll give that a shot
date_prop: Union[Unset, datetime.date] = isoparse("1010-10-10").date(), | ||
float_prop: Union[Unset, float] = 3.14, | ||
int_prop: Union[Unset, int] = 7, | ||
boolean_prop: Union[Unset, bool] = 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.
This default used to be False, was that a mistake or did we change the spec?
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 self.default
fails to catch the default of False... oopsie
elif self.nullable: | ||
default = "None" |
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.
Maybe we should leave it for now to not mess up existing clients, but now that we have UNSET, I don't think we want to default to None. The only reason that was the behavior was because it took the place of UNSET, but if None is a legitimate value that will get passed up to the API, I think it should have to be specified.
What do you think?
openapi_python_client/templates/property_templates/union_property.pyi
Outdated
Show resolved
Hide resolved
openapi_python_client/templates/property_templates/union_property.pyi
Outdated
Show resolved
Hide resolved
@@ -55,7 +55,7 @@ isort .\ | |||
&& flake8 openapi_python_client\ | |||
&& safety check --bare\ | |||
&& mypy openapi_python_client\ | |||
&& pytest --cov openapi_python_client tests\ | |||
&& pytest --cov openapi_python_client tests --cov-report=term-missing\ |
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 that do?
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.
When it shows the per-file coverage percentages in the report, it also shows which line numbers are missing next to it which is an absolute godsend
Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com>
…d but nullable properties no longer have a default value
@dbanty Main merged, bool default fixed, required but nullable defaults removed, and non-required Union type strings are less ugly now - should be good to go (hopefully lol) |
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 found another bug, sorry 😰
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.
Approved! 🎉 Squashing onto main, will make it into 0.7.0 after I finish #236 .
Thank you so much for this @emannguitar and @dbanty! Sorry for not responding, I was on the road and not checking notifications. |
Big thanks to @bowenwr for his groundwork for the
UNSET
stuff! I took a lot of inspiration from his fix + added further separation of required vs. nullable to get around some of the edge cases such as query & path params.Closes #205, #228