Skip to content

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

Conversation

emann
Copy link
Collaborator

@emann emann commented Nov 2, 2020

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

@codecov
Copy link

codecov bot commented Nov 2, 2020

Codecov Report

Merging #230 into main will not change coverage.
The diff coverage is 100.00%.

Impacted file tree graph

@@            Coverage Diff            @@
##              main      #230   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           41        41           
  Lines         1307      1332   +25     
=========================================
+ Hits          1307      1332   +25     
Impacted Files Coverage Δ
openapi_python_client/parser/openapi.py 100.00% <100.00%> (ø)
openapi_python_client/parser/properties.py 100.00% <100.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 7a483dc...2a8c51f. Read the comment docs.

Copy link
Collaborator

@dbanty dbanty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple things:

  1. 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.
  2. 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),
Copy link
Collaborator

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())
Copy link
Collaborator

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.

Copy link
Collaborator

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 UNSETs, we should class Unset: to make it possible.

Comment on lines 21 to 24
include: Optional[Set[str]] = None,
exclude: Optional[Set[str]] = None,
exclude_unset: bool = False,
exclude_none: bool = False,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple notes here:

  1. 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.
  2. When would we ever want to not exclude_unset?

My instinct is to scratch these params and always skip values that are UNSET.

Copy link
Collaborator Author

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

emann and others added 2 commits November 3, 2020 11:39
Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com>
Comment on lines 39 to 44

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:
Copy link
Collaborator Author

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?

Copy link
Collaborator

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 dbanty added this to the 0.7.0 milestone Nov 4, 2020
@emann
Copy link
Collaborator Author

emann commented Nov 4, 2020

@dbanty Ready for another review pass - unfortunately mypy didn't like Literal[...] (even if I did), so I went with something similar to what I had mentioned which turned out to not be too gross to implement.

Copy link
Collaborator

@dbanty dbanty left a 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]]
Copy link
Collaborator

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.

Copy link
Collaborator Author

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,
Copy link
Collaborator

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?

Copy link
Collaborator Author

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

Comment on lines 79 to 80
elif self.nullable:
default = "None"
Copy link
Collaborator

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?

@@ -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\
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's that do?

Copy link
Collaborator Author

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

emann and others added 3 commits November 6, 2020 09:23
Co-authored-by: Dylan Anthony <43723790+dbanty@users.noreply.github.com>
…d but nullable properties no longer have a default value
@emann
Copy link
Collaborator Author

emann commented Nov 6, 2020

@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)

Copy link
Collaborator

@dbanty dbanty left a 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 😰

@dbanty dbanty mentioned this pull request Nov 6, 2020
8 tasks
Copy link
Collaborator

@dbanty dbanty left a 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 .

@bowenwr
Copy link
Contributor

bowenwr commented Nov 13, 2020

Thank you so much for this @emannguitar and @dbanty! Sorry for not responding, I was on the road and not checking notifications.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Remove optional generated class attributes set to None from serialized payload
3 participants