Skip to content

Commit b4a9f85

Browse files
committed
fix: Multipart uploads for httpx >= 0.19.0
Fixes #508 BREAKING CHANGE: `File` uploads can now only accept binary payloads (`BinaryIO`).
1 parent c9a4d03 commit b4a9f85

12 files changed

+67
-54
lines changed

end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py

+15-11
Original file line numberDiff line numberDiff line change
@@ -102,36 +102,40 @@ def to_dict(self) -> Dict[str, Any]:
102102
def to_multipart(self) -> Dict[str, Any]:
103103
some_file = self.some_file.to_tuple()
104104

105-
some_object = (None, json.dumps(self.some_object.to_dict()), "application/json")
105+
some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json")
106106

107107
some_optional_file: Union[Unset, FileJsonType] = UNSET
108108
if not isinstance(self.some_optional_file, Unset):
109109
some_optional_file = self.some_optional_file.to_tuple()
110110

111-
some_string = self.some_string if self.some_string is UNSET else (None, str(self.some_string), "text/plain")
112-
some_number = self.some_number if self.some_number is UNSET else (None, str(self.some_number), "text/plain")
113-
some_array: Union[Unset, Tuple[None, str, str]] = UNSET
111+
some_string = (
112+
self.some_string if self.some_string is UNSET else (None, str(self.some_string).encode(), "text/plain")
113+
)
114+
some_number = (
115+
self.some_number if self.some_number is UNSET else (None, str(self.some_number).encode(), "text/plain")
116+
)
117+
some_array: Union[Unset, Tuple[None, bytes, str]] = UNSET
114118
if not isinstance(self.some_array, Unset):
115119
_temp_some_array = self.some_array
116-
some_array = (None, json.dumps(_temp_some_array), "application/json")
120+
some_array = (None, json.dumps(_temp_some_array).encode(), "application/json")
117121

118-
some_optional_object: Union[Unset, Tuple[None, str, str]] = UNSET
122+
some_optional_object: Union[Unset, Tuple[None, bytes, str]] = UNSET
119123
if not isinstance(self.some_optional_object, Unset):
120-
some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()), "application/json")
124+
some_optional_object = (None, json.dumps(self.some_optional_object.to_dict()).encode(), "application/json")
121125

122126
some_nullable_object = (
123-
(None, json.dumps(self.some_nullable_object.to_dict()), "application/json")
127+
(None, json.dumps(self.some_nullable_object.to_dict()).encode(), "application/json")
124128
if self.some_nullable_object
125129
else None
126130
)
127131

128-
some_enum: Union[Unset, Tuple[None, str, str]] = UNSET
132+
some_enum: Union[Unset, Tuple[None, bytes, str]] = UNSET
129133
if not isinstance(self.some_enum, Unset):
130-
some_enum = (None, str(self.some_enum.value), "text/plain")
134+
some_enum = (None, str(self.some_enum.value).encode(), "text/plain")
131135

132136
field_dict: Dict[str, Any] = {}
133137
for prop_name, prop in self.additional_properties.items():
134-
field_dict[prop_name] = (None, json.dumps(prop.to_dict()), "application/json")
138+
field_dict[prop_name] = (None, json.dumps(prop.to_dict()).encode(), "application/json")
135139

136140
field_dict.update(
137141
{

end_to_end_tests/golden-record/my_test_api_client/types.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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, Optional, Tuple, TypeVar
33

44
import attr
55

@@ -11,14 +11,14 @@ def __bool__(self) -> bool:
1111

1212
UNSET: Unset = Unset()
1313

14-
FileJsonType = Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]]
14+
FileJsonType = Tuple[Optional[str], BinaryIO, Optional[str]]
1515

1616

1717
@attr.s(auto_attribs=True)
1818
class File:
1919
"""Contains information for file uploads"""
2020

21-
payload: Union[BinaryIO, TextIO]
21+
payload: BinaryIO
2222
file_name: Optional[str] = None
2323
mime_type: Optional[str] = None
2424

openapi_python_client/templates/model.py.jinja

+5-4
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,10 @@ class {{ class_name }}:
6868
{% for property in model.required_properties + model.optional_properties %}
6969
{% if property.template %}
7070
{% from "property_templates/" + property.template import transform %}
71-
{{ transform(property, "self." + property.python_name, property.python_name, stringify=multipart) }}
71+
{# Stopped here #}
72+
{{ transform(property, "self." + property.python_name, property.python_name, multipart=multipart) }}
7273
{% elif multipart %}
73-
{{ property.python_name }} = self.{{ property.python_name }} if self.{{ property.python_name }} is UNSET else (None, str(self.{{ property.python_name }}), "text/plain")
74+
{{ property.python_name }} = self.{{ property.python_name }} if self.{{ property.python_name }} is UNSET else (None, str(self.{{ property.python_name }}).encode(), "text/plain")
7475
{% else %}
7576
{{ property.python_name }} = self.{{ property.python_name }}
7677
{% endif %}
@@ -81,10 +82,10 @@ field_dict: Dict[str, Any] = {}
8182
{% if model.additional_properties.template %}
8283
{% from "property_templates/" + model.additional_properties.template import transform %}
8384
for prop_name, prop in self.additional_properties.items():
84-
{{ transform(model.additional_properties, "prop", "field_dict[prop_name]", stringify=multipart) | indent(4) }}
85+
{{ transform(model.additional_properties, "prop", "field_dict[prop_name]", multipart=multipart) | indent(4) }}
8586
{% elif multipart %}
8687
field_dict.update({
87-
key: (None, str(value), "text/plain")
88+
key: (None, str(value).encode(), "text/plain")
8889
for key, value in self.additional_properties.items()
8990
})
9091
{% else %}

openapi_python_client/templates/property_templates/any_property.py.jinja

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
{{ property.python_name }} = {{ source }}
33
{% endmacro %}
44

5-
{% macro transform(property, source, destination, declare_type=True, stringify=False) %}
5+
{% macro transform(property, source, destination, declare_type=True, multipart=False) %}
66
{{ destination }} = {{ source }}
77
{% endmacro %}

openapi_python_client/templates/property_templates/date_property.py.jinja

+8-4
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@ isoparse({{ source }}).date()
1010

1111
{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %}
1212

13-
{% macro transform(property, source, destination, declare_type=True, stringify=False) %}
13+
{% macro transform(property, source, destination, declare_type=True, multipart=False) %}
14+
{% set transformed = source + ".isoformat()" %}
15+
{% if multipart %} {# Multipart data must be bytes, not str #}
16+
{% set transformed = transformed + ".encode()" %}
17+
{% endif %}
1418
{% if property.required %}
15-
{{ destination }} = {{ source }}.isoformat() {% if property.nullable %}if {{ source }} else None {%endif%}
19+
{{ destination }} = {{ transformed }} {% if property.nullable %}if {{ source }} else None {%endif%}
1620
{% else %}
1721
{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET
1822
if not isinstance({{ source }}, Unset):
1923
{% if property.nullable %}
20-
{{ destination }} = {{ source }}.isoformat() if {{ source }} else None
24+
{{ destination }} = {{ transformed }} if {{ source }} else None
2125
{% else %}
22-
{{ destination }} = {{ source }}.isoformat()
26+
{{ destination }} = {{ transformed }}
2327
{% endif %}
2428
{% endif %}
2529
{% endmacro %}

openapi_python_client/templates/property_templates/datetime_property.py.jinja

+9-5
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,24 @@ isoparse({{ source }})
1010

1111
{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, str){% endmacro %}
1212

13-
{% macro transform(property, source, destination, declare_type=True, stringify=False) %}
13+
{% macro transform(property, source, destination, declare_type=True, multipart=False) %}
14+
{% set transformed = source + ".isoformat()" %}
15+
{% if multipart %} {# Multipart data must be bytes, not str #}
16+
{% set transformed = transformed + ".encode()" %}
17+
{% endif %}
1418
{% if property.required %}
1519
{% if property.nullable %}
16-
{{ destination }} = {{ source }}.isoformat() if {{ source }} else None
20+
{{ destination }} = {{ transformed }} if {{ source }} else None
1721
{% else %}
18-
{{ destination }} = {{ source }}.isoformat()
22+
{{ destination }} = {{ transformed }}
1923
{% endif %}
2024
{% else %}
2125
{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %} = UNSET
2226
if not isinstance({{ source }}, Unset):
2327
{% if property.nullable %}
24-
{{ destination }} = {{ source }}.isoformat() if {{ source }} else None
28+
{{ destination }} = {{ transformed }} if {{ source }} else None
2529
{% else %}
26-
{{ destination }} = {{ source }}.isoformat()
30+
{{ destination }} = {{ transformed }}
2731
{% endif %}
2832
{% endif %}
2933
{% endmacro %}

openapi_python_client/templates/property_templates/enum_property.py.jinja

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010

1111
{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, {{ property.value_type.__name__ }}){% endmacro %}
1212

13-
{% macro transform(property, source, destination, declare_type=True, stringify=False) %}
13+
{% macro transform(property, source, destination, declare_type=True, multipart=False) %}
1414
{% set transformed = source + ".value" %}
1515
{% set type_string = property.get_type_string(json=True) %}
16-
{% if stringify %}
17-
{% set transformed = "(None, str(" + transformed + "), 'text/plain')" %}
18-
{% set type_string = "Union[Unset, Tuple[None, str, str]]" %}
16+
{% if multipart %}
17+
{% set transformed = "(None, str(" + transformed + ").encode(), \"text/plain\")" %}
18+
{% set type_string = "Union[Unset, Tuple[None, bytes, str]]" %}
1919
{% endif %}
2020
{% if property.required %}
2121
{% if property.nullable %}

openapi_python_client/templates/property_templates/file_property.py.jinja

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ File(
1212

1313
{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, bytes){% endmacro %}
1414

15-
{% macro transform(property, source, destination, declare_type=True, stringify=False) %}
15+
{% macro transform(property, source, destination, declare_type=True, multipart=False) %}
1616
{% if property.required %}
1717
{% if property.nullable %}
1818
{{ destination }} = {{ source }}.to_tuple() if {{ source }} else None

openapi_python_client/templates/property_templates/list_property.py.jinja

+12-12
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ for {{ inner_source }} in (_{{ property.python_name }} or []):
1717
{% endif %}
1818
{% endmacro %}
1919

20-
{% macro _transform(property, source, destination, stringify, transform_method) %}
20+
{% macro _transform(property, source, destination, multipart, transform_method) %}
2121
{% set inner_property = property.inner_property %}
22-
{% if stringify %}
23-
{% set stringified_destination = destination %}
22+
{% if multipart %}
23+
{% set multipart_destination = destination %}
2424
{% set destination = "_temp_" + destination %}
2525
{% endif %}
2626
{% if inner_property.template %}
@@ -33,17 +33,17 @@ for {{ inner_source }} in {{ source }}:
3333
{% else %}
3434
{{ destination }} = {{ source }}
3535
{% endif %}
36-
{% if stringify %}
37-
{{ stringified_destination }} = (None, json.dumps({{ destination }}), 'application/json')
36+
{% if multipart %}
37+
{{ multipart_destination }} = (None, json.dumps({{ destination }}).encode(), 'application/json')
3838
{% endif %}
3939
{% endmacro %}
4040

4141
{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, list){% endmacro %}
4242

43-
{% macro transform(property, source, destination, declare_type=True, stringify=False, transform_method="to_dict") %}
43+
{% macro transform(property, source, destination, declare_type=True, multipart=False, transform_method="to_dict") %}
4444
{% set inner_property = property.inner_property %}
45-
{% if stringify %}
46-
{% set type_string = "Union[Unset, Tuple[None, str, str]]" %}
45+
{% if multipart %}
46+
{% set type_string = "Union[Unset, Tuple[None, bytes, str]]" %}
4747
{% else %}
4848
{% set type_string = property.get_type_string(json=True) %}
4949
{% endif %}
@@ -52,9 +52,9 @@ for {{ inner_source }} in {{ source }}:
5252
if {{ source }} is None:
5353
{{ destination }} = None
5454
else:
55-
{{ _transform(property, source, destination, stringify, transform_method) | indent(4) }}
55+
{{ _transform(property, source, destination, multipart, transform_method) | indent(4) }}
5656
{% else %}
57-
{{ _transform(property, source, destination, stringify, transform_method) }}
57+
{{ _transform(property, source, destination, multipart, transform_method) }}
5858
{% endif %}
5959
{% else %}
6060
{{ destination }}{% if declare_type %}: {{ type_string }}{% endif %} = UNSET
@@ -63,9 +63,9 @@ if not isinstance({{ source }}, Unset):
6363
if {{ source }} is None:
6464
{{ destination }} = None
6565
else:
66-
{{ _transform(property, source, destination, stringify, transform_method) | indent(8)}}
66+
{{ _transform(property, source, destination, multipart, transform_method) | indent(8)}}
6767
{% else %}
68-
{{ _transform(property, source, destination, stringify, transform_method) | indent(4)}}
68+
{{ _transform(property, source, destination, multipart, transform_method) | indent(4)}}
6969
{% endif %}
7070
{% endif %}
7171

openapi_python_client/templates/property_templates/model_property.py.jinja

+4-4
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
{% macro check_type_for_construct(property, source) %}isinstance({{ source }}, dict){% endmacro %}
1212

13-
{% macro transform(property, source, destination, declare_type=True, stringify=False, transform_method="to_dict") %}
13+
{% macro transform(property, source, destination, declare_type=True, multipart=False, transform_method="to_dict") %}
1414
{% set transformed = source + "." + transform_method + "()" %}
15-
{% if stringify %}
16-
{% set transformed = "(None, json.dumps(" + transformed + "), 'application/json')" %}
17-
{% set type_string = "Union[Unset, Tuple[None, str, str]]" %}
15+
{% if multipart %}
16+
{% set transformed = "(None, json.dumps(" + transformed + ").encode(), 'application/json')" %}
17+
{% set type_string = "Union[Unset, Tuple[None, bytes, str]]" %}
1818
{% else %}
1919
{% set type_string = property.get_type_string(json=True) %}
2020
{% endif %}

openapi_python_client/templates/property_templates/union_property.py.jinja

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def _parse_{{ property.python_name }}(data: object) -> {{ property.get_type_stri
3535
{{ property.python_name }} = _parse_{{ property.python_name }}({{ source }})
3636
{% endmacro %}
3737

38-
{% macro transform(property, source, destination, declare_type=True, stringify=False) %}
38+
{% macro transform(property, source, destination, declare_type=True, multipart=False) %}
3939
{% if not property.required or property.nullable %}
4040
{{ destination }}{% if declare_type %}: {{ property.get_type_string(json=True) }}{% endif %}
4141

@@ -61,7 +61,7 @@ elif isinstance({{ source }}, {{ inner_property.get_instance_type_string() }}):
6161
else:
6262
{% endif %}
6363
{% from "property_templates/" + inner_property.template import transform %}
64-
{{ transform(inner_property, source, destination, declare_type=False, stringify=stringify) | indent(4) }}
64+
{{ transform(inner_property, source, destination, declare_type=False, multipart=multipart) | indent(4) }}
6565
{% endfor %}
6666
{% if property.has_properties_without_templates and (property.inner_properties_with_template() | any or not property.required)%}
6767
else:

openapi_python_client/templates/types.py.jinja

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

44
import attr
55

@@ -12,14 +12,14 @@ class Unset:
1212
UNSET: Unset = Unset()
1313

1414
{# Used as `FileProperty._json_type_string` #}
15-
FileJsonType = Tuple[Optional[str], Union[BinaryIO, TextIO], Optional[str]]
15+
FileJsonType = Tuple[Optional[str], BinaryIO, Optional[str]]
1616

1717

1818
@attr.s(auto_attribs=True)
1919
class File:
2020
""" Contains information for file uploads """
2121

22-
payload: Union[BinaryIO, TextIO]
22+
payload: BinaryIO
2323
file_name: Optional[str] = None
2424
mime_type: Optional[str] = None
2525

0 commit comments

Comments
 (0)