Skip to content

Commit e9a3ee5

Browse files
committed
Allow Unset values to avoid being send during serialization
1 parent cbff87f commit e9a3ee5

File tree

5 files changed

+144
-31
lines changed

5 files changed

+144
-31
lines changed

openapi_python_client/parser/properties.py

+84-18
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,16 @@ def get_type_string(self, no_optional: bool = False) -> str:
5151
Args:
5252
no_optional: Do not include Optional even if the value is optional (needed for isinstance checks)
5353
"""
54-
if no_optional or (self.required and not self.nullable):
55-
return self._type_string
56-
return f"Optional[{self._type_string}]"
54+
return type_string(no_optional, self, self._type_string)
55+
56+
def get_query_string(self, no_optional: bool = False) -> str:
57+
"""
58+
Get a string representation of type that should be used when the property is represented in a query string.
59+
60+
Args:
61+
no_optional: Do not include Optional even if the value is optional (needed for isinstance checks)
62+
"""
63+
return query_string(no_optional, self, self._type_string)
5764

5865
def get_imports(self, *, prefix: str) -> Set[str]:
5966
"""
@@ -63,24 +70,42 @@ def get_imports(self, *, prefix: str) -> Set[str]:
6370
prefix: A prefix to put before any relative (local) module names. This should be the number of . to get
6471
back to the root of the generated client.
6572
"""
66-
if self.nullable or not self.required:
73+
if self.required and self.nullable:
6774
return {"from typing import Optional"}
75+
elif not self.required:
76+
return {"from typing import Union",
77+
f"from {prefix}types import Unset",
78+
f"from {prefix}types import UNSET"}
6879
return set()
6980

7081
def to_string(self) -> str:
7182
""" How this should be declared in a dataclass """
7283
if self.default:
7384
default = self.default
7485
elif not self.required:
75-
default = "None"
86+
default = "UNSET"
7687
else:
7788
default = None
7889

7990
if default is not None:
80-
return f"{self.python_name}: {self.get_type_string()} = {self.default}"
91+
return f"{self.python_name}: {self.get_type_string()} = {default}"
8192
else:
8293
return f"{self.python_name}: {self.get_type_string()}"
8394

95+
def to_query_string(self) -> str:
96+
""" How this should be declared in a query string """
97+
if self.default:
98+
default = self.default
99+
elif not self.required:
100+
default = "None"
101+
else:
102+
default = None
103+
104+
if default is not None:
105+
return f"{self.python_name}: {self.get_query_string()} = {default}"
106+
else:
107+
return f"{self.python_name}: {self.get_query_string()}"
108+
84109

85110
@dataclass
86111
class StringProperty(Property):
@@ -222,9 +247,13 @@ class ListProperty(Property, Generic[InnerProp]):
222247

223248
def get_type_string(self, no_optional: bool = False) -> str:
224249
""" Get a string representation of type that should be used when declaring this property """
225-
if no_optional or (self.required and not self.nullable):
226-
return f"List[{self.inner_property.get_type_string()}]"
227-
return f"Optional[List[{self.inner_property.get_type_string()}]]"
250+
return type_string(no_optional, self, f"List[{self.inner_property.get_type_string()}]")
251+
252+
def get_query_string(self, no_optional: bool = False) -> str:
253+
"""
254+
Get a string representation of type that should be used when the property is represented in a query string.
255+
"""
256+
return query_string(no_optional, self, f"List[{self.inner_property.get_query_string()}]")
228257

229258
def get_imports(self, *, prefix: str) -> Set[str]:
230259
"""
@@ -254,9 +283,15 @@ def get_type_string(self, no_optional: bool = False) -> str:
254283
""" Get a string representation of type that should be used when declaring this property """
255284
inner_types = [p.get_type_string() for p in self.inner_properties]
256285
inner_prop_string = ", ".join(inner_types)
257-
if no_optional or (self.required and not self.nullable):
258-
return f"Union[{inner_prop_string}]"
259-
return f"Optional[Union[{inner_prop_string}]]"
286+
return type_string(no_optional, self, inner_prop_string, is_outer_union=True)
287+
288+
def get_query_string(self, no_optional: bool = False) -> str:
289+
"""
290+
Get a string representation of type that should be used when the property is represented in a query string.
291+
"""
292+
inner_types = [p.get_query_string() for p in self.inner_properties]
293+
inner_prop_string = ", ".join(inner_types)
294+
return query_string(no_optional, self, inner_prop_string, is_outer_union=True)
260295

261296
def get_imports(self, *, prefix: str) -> Set[str]:
262297
"""
@@ -328,10 +363,13 @@ def get_enum(name: str) -> Optional["EnumProperty"]:
328363

329364
def get_type_string(self, no_optional: bool = False) -> str:
330365
""" Get a string representation of type that should be used when declaring this property """
366+
return type_string(no_optional, self, self.reference.class_name)
331367

332-
if no_optional or (self.required and not self.nullable):
333-
return self.reference.class_name
334-
return f"Optional[{self.reference.class_name}]"
368+
def get_query_string(self, no_optional: bool = False) -> str:
369+
"""
370+
Get a string representation of type that should be used when the property is represented in a query string.
371+
"""
372+
return query_string(no_optional, self, self.reference.class_name)
335373

336374
def get_imports(self, *, prefix: str) -> Set[str]:
337375
"""
@@ -390,9 +428,13 @@ def template(self) -> str: # type: ignore
390428

391429
def get_type_string(self, no_optional: bool = False) -> str:
392430
""" Get a string representation of type that should be used when declaring this property """
393-
if no_optional or (self.required and not self.nullable):
394-
return self.reference.class_name
395-
return f"Optional[{self.reference.class_name}]"
431+
return type_string(no_optional, self, self.reference.class_name)
432+
433+
def get_query_string(self, no_optional: bool = False) -> str:
434+
"""
435+
Get a string representation of type that should be used when the property is represented in a query string.
436+
"""
437+
return query_string(no_optional, self, self.reference.class_name)
396438

397439
def get_imports(self, *, prefix: str) -> Set[str]:
398440
"""
@@ -575,3 +617,27 @@ def property_from_data(
575617
return _property_from_data(name=name, required=required, data=data)
576618
except ValidationError:
577619
return PropertyError(detail="Failed to validate default value", data=data)
620+
621+
622+
def type_string(no_optional: bool, prop: Property, inner_type_string: str, is_outer_union: bool = False) -> str:
623+
if no_optional or (prop.required and not prop.nullable):
624+
if is_outer_union:
625+
return f"Union[{inner_type_string}]"
626+
return inner_type_string
627+
elif prop.nullable and prop.required:
628+
if is_outer_union:
629+
return f"Optional[Union[{inner_type_string}]]"
630+
return f"Optional[{inner_type_string}]"
631+
elif not prop.nullable and not prop.required:
632+
return f"Union[Unset, None, {inner_type_string}]"
633+
return f"Union[Unset, {inner_type_string}]"
634+
635+
636+
def query_string(no_optional: bool, prop: Property, inner_type_string: str, is_outer_union: bool = False) -> str:
637+
if no_optional or (prop.required and not prop.nullable):
638+
if is_outer_union:
639+
return f"Union[{inner_type_string}]"
640+
return inner_type_string
641+
if is_outer_union:
642+
return f"Optional[Union[{inner_type_string}]]"
643+
return f"Optional[{inner_type_string}]"

openapi_python_client/templates/endpoint_macros.pyi

+3-3
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ client: Client,
8080
{% endif %}
8181
{# path parameters #}
8282
{% for parameter in endpoint.path_parameters %}
83-
{{ parameter.to_string() }},
83+
{{ parameter.to_query_string() }},
8484
{% endfor %}
8585
{# Form data if any #}
8686
{% if endpoint.form_body_reference %}
@@ -96,10 +96,10 @@ json_body: {{ endpoint.json_body.get_type_string() }},
9696
{% endif %}
9797
{# query parameters #}
9898
{% for parameter in endpoint.query_parameters %}
99-
{{ parameter.to_string() }},
99+
{{ parameter.to_query_string() }},
100100
{% endfor %}
101101
{% for parameter in endpoint.header_parameters %}
102-
{{ parameter.to_string() }},
102+
{{ parameter.to_query_string() }},
103103
{% endfor %}
104104
{% endmacro %}
105105

openapi_python_client/templates/model.pyi

+11-5
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,17 @@ class {{ model.reference.class_name }}:
2424
{% endif %}
2525
{% endfor %}
2626

27-
return {
28-
{% for property in model.required_properties + model.optional_properties %}
29-
"{{ property.name }}": {{ property.python_name }},
30-
{% endfor %}
31-
}
27+
properties: Dict[str, Any] = dict()
28+
29+
{% for property in model.required_properties + model.optional_properties %}
30+
{% if not property.required %}
31+
if {{property.python_name}} != UNSET:
32+
properties["{{ property.name }}"] = {{ property.python_name }}
33+
{% else %}
34+
properties["{{ property.name }}"] = {{ property.python_name }}
35+
{% endif %}
36+
{% endfor %}
37+
return properties
3238

3339
@staticmethod
3440
def from_dict(d: Dict[str, Any]) -> "{{ model.reference.class_name }}":

openapi_python_client/templates/types.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
""" Contains some shared types for properties """
2-
from typing import BinaryIO, Generic, MutableMapping, Optional, TextIO, Tuple, TypeVar, Union
2+
from typing import BinaryIO, Generic, MutableMapping, NewType, Optional, TextIO, Tuple, TypeVar, Union
33

44
import attr
55

6+
Unset = NewType("Unset", object)
7+
UNSET: Unset = object()
8+
69

710
@attr.s(auto_attribs=True)
811
class File:

tests/test_openapi_parser/test_properties.py

+42-4
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,23 @@ def test_get_type_string(self):
2424
p = Property(name="test", required=True, default=None, nullable=False)
2525
p._type_string = "TestType"
2626

27+
# Nullable = False, Required = True
2728
assert p.get_type_string() == "TestType"
29+
30+
# Nullable = False, Required = False, No Default
31+
p.nullable = False
2832
p.required = False
29-
assert p.get_type_string() == "Optional[TestType]"
33+
assert p.get_type_string() == "Union[Unset, None, TestType]"
3034
assert p.get_type_string(True) == "TestType"
3135

36+
# Nullable = True, Required = False, No Default
3237
p.required = False
3338
p.nullable = True
39+
assert p.get_type_string() == "Union[Unset, TestType]"
40+
41+
# Nullable = True, Required = True, No Default
42+
p.required = True
43+
p.nullable = True
3444
assert p.get_type_string() == "Optional[TestType]"
3545

3646
def test_to_string(self, mocker):
@@ -42,12 +52,33 @@ def test_to_string(self, mocker):
4252
get_type_string = mocker.patch.object(p, "get_type_string")
4353

4454
assert p.to_string() == f"{snake_case(name)}: {get_type_string()}"
45-
p.required = False
46-
assert p.to_string() == f"{snake_case(name)}: {get_type_string()} = None"
55+
p.required = True
56+
p.nullable = True
57+
assert p.to_string() == f"{snake_case(name)}: {get_type_string()}"
4758

4859
p.default = "TEST"
4960
assert p.to_string() == f"{snake_case(name)}: {get_type_string()} = TEST"
5061

62+
p.nullable = False
63+
p.required = False
64+
p.default = None
65+
assert p.to_string() == f"{snake_case(name)}: {get_type_string()} = UNSET"
66+
67+
def test_to_query_string(self, mocker):
68+
from openapi_python_client.parser.properties import Property
69+
70+
name = mocker.MagicMock()
71+
snake_case = mocker.patch("openapi_python_client.utils.snake_case")
72+
p = Property(name=name, required=True, default=None, nullable=False)
73+
get_query_string = mocker.patch.object(p, "get_query_string")
74+
75+
assert p.to_query_string() == f"{snake_case(name)}: {get_query_string()}"
76+
p.required = False
77+
assert p.to_query_string() == f"{snake_case(name)}: {get_query_string()} = None"
78+
79+
p.default = "TEST"
80+
assert p.to_query_string() == f"{snake_case(name)}: {get_query_string()} = TEST"
81+
5182
def test_get_imports(self, mocker):
5283
from openapi_python_client.parser.properties import Property
5384

@@ -56,9 +87,16 @@ def test_get_imports(self, mocker):
5687
p = Property(name=name, required=True, default=None, nullable=False)
5788
assert p.get_imports(prefix="") == set()
5889

59-
p.required = False
90+
p.nullable = True
6091
assert p.get_imports(prefix="") == {"from typing import Optional"}
6192

93+
p.required = False
94+
assert p.get_imports(prefix="..") == {
95+
"from typing import Union",
96+
"from ..types import Unset",
97+
"from ..types import UNSET",
98+
}
99+
62100
def test__validate_default(self):
63101
from openapi_python_client.parser.properties import Property
64102

0 commit comments

Comments
 (0)