-
Notifications
You must be signed in to change notification settings - Fork 1
Allow Unset values to avoid being send during serialization #10
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
4bdadd6
to
e9a3ee5
Compare
FYI @benchling/ce-team and @rswang since Daniel is out |
Unset = NewType("Unset", object) | ||
UNSET: 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.
Shouldn't this be
Unset = NewType("Unset", object) | |
UNSET: Unset = object() | |
Unset = NewType("Unset", object) | |
UNSET: Unset = Unset(object()) |
(not that it matters)
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 guess another alternative would be to keep it as UNSET = object()
and use Literal[UNSET]
as the type? (Also doesn't seem like it matters)
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 I tried that previously, PyCharm complained:
'Literal' may be parameterized with literal ints, byte and unicode strings, bools, Enum values, None, other literal types, or type aliases to other literal types
else: | ||
return f"{self.python_name}: {self.get_type_string()}" | ||
|
||
def to_query_string(self) -> 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.
This threw me for a while - I thought get_query_string
was building literal query strings, e,g. ?property=value
which this is... not
I think you should rename this to something like
def to_query_string(self) -> str: | |
def to_query_method_arg(self) -> str: |
or
def to_query_string(self) -> str: | |
def to_query_parameter(self) -> str: |
(and get_query_string()
/query_string()
to match)
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, I renamed it. It's unclear to me whether the "new" query behavior should be the default the this response body/model body should be the new method, but it seems to me like it's used in more places.
I also had already changed it and then had to put back the query parsing....
|
||
{% for property in model.required_properties + model.optional_properties %} | ||
{% if not property.required %} | ||
if {{property.python_name}} != 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.
You should use instance equality with a sentinel object
if {{property.python_name}} != UNSET: | |
if {{property.python_name}} is not 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.
LGTM, just the overlooked rename
{% endfor %} | ||
{% for parameter in endpoint.header_parameters %} | ||
{{ parameter.to_string() }}, | ||
{{ parameter.to_query_string() }}, |
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 you forgot to update this one
{{ parameter.to_query_string() }}, | |
{{ parameter.to_query_method_arg() }}, |
FYI @mzhu22 |
One of the major drawbacks to this approach is that it applies to both request and response bodies. When really we only want it to apply to request bodies. I don't think there's a way to really control this via the spec without misusing it. Do we think it would be better to leave the original typings as is and only change the defaults to |
@bowenwr good point. Maybe it would be better to leave The response objects still have |
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.
👏 yay! Are you going to also try to submit this PR upstream? Seems like a useful addition for others as well.
@@ -24,13 +24,23 @@ def test_get_type_string(self): | |||
p = Property(name="test", required=True, default=None, nullable=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.
Is there any way we can have a more end-to-end test that unset values are not sent over to the API endpoint?
I'm a bit nervous since I'm very unfamiliar with this codebase.
@@ -36,7 +38,7 @@ else: | |||
{% endif %} | |||
{% if inner_property.template %} | |||
{% from "property_templates/" + inner_property.template import transform %} | |||
{{ transform(inner_property, source, destination) | indent(8) }} | |||
{{ transform(inner_property, source, destination) | indent(4) }} |
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 caused this indentation level to change?
if {{property.python_name}} is not UNSET: | ||
properties["{{ property.name }}"] = {{ property.python_name }} | ||
{% else %} | ||
properties["{{ property.name }}"] = {{ property.python_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.
Is it possible that a property is both required AND unset, and if so is it desired that it would be exposed in the dictionary directly (instead of, say, erroring)?
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 a property is required, we shouldn't allow unset - we simply shouldn't allow a default. It could still be nullable and required, in which case we should allow it to be None
.
if no_optional or (self.required and not self.nullable): | ||
return self._type_string | ||
return f"Optional[{self._type_string}]" | ||
return type_string(no_optional, self, self._type_string) |
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.
Could this be a method on Property
instead of a function that takes in self
? (Same for query_method_arg
)
@@ -63,24 +70,42 @@ def get_imports(self, *, prefix: str) -> Set[str]: | |||
prefix: A prefix to put before any relative (local) module names. This should be the number of . to get | |||
back to the root of the generated client. | |||
""" | |||
if self.nullable or not self.required: | |||
if self.required and self.nullable: | |||
return {"from typing import Optional"} |
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.required
is true, why do we need to import Optional?
|
||
|
||
def query_method_arg(no_optional: bool, prop: Property, inner_type_string: str, is_outer_union: bool = False) -> str: | ||
if no_optional or (prop.required and not prop.nullable): |
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.
Potentially slightly more readable as:
type_string = inner_type_string
if is_outer_union:
type_string = f"Union[{type_string}]"
if not no_optional and (not prop.required or prop.nullable):
type_string = f"Optional[{type_string}]"
return type_string
Similarly for type_string
above. Don't feel too strongly though, will leave it to you.
if is_outer_union: | ||
return f"Optional[Union[{inner_type_string}]]" | ||
return f"Optional[{inner_type_string}]" | ||
elif not prop.nullable and not prop.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.
Should this be elif prop.nullable and not prop.required
, given that we're unioning it with None
in L632?
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 dropped this change in the latest commit where I preserve the original typing and just set the default.
@@ -12,6 +12,9 @@ if {{ source }} is not None: | |||
{% if property.required %} | |||
{{ destination }} = {{ source }} | |||
{% else %} | |||
{{ destination }} = {{ source }} if {{ source }} else None | |||
if {{ source }} is 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.
For my context, where is the macro transform
actually called?
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's called frequently in the various .pyi
templates - you'll find it easily if you search and make sure you aren't filtering to only .py
files.
I liked this suggestion, I implemented it and tested it out. It seems to pass our integration tests. I still needed |
I'd like to upstream this at some point though it would probably need considerably more polish and E2E tests. I'll probably incubate it a bit internally too to make sure I haven't missed any cases as we expand our spec coverage. |
9dcce0c
to
d83e554
Compare
Fixes openapi-generators#205.
Introduces a new sentinel value,
UNSET
which becomes the new default fornullable
fields which aren'trequired
. If a field is set to this value, it will not be sent as part of the payload during serialization.Since we still want to treat endpoint arguments and query params as they were originally without
Unset
, this logic has moved toto_query_string
onProperty
.Given a
str
type, a nullable property which isn't required might look something like: