We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add support for streaming multipart/form-data
(After initial discussion in https://github.com/encode/httpx/discussions/2227.)

As far as I can see, there's no way to stream `multipart/form-data` uploads when the `Content-Length` is unknown, since HTTPX tries to get body size some way or another (last resort is to read it all in memory and count). Please, let me know if I overlooked something!

Use case would be to stream something whose size is unknown beforehand, like fetching and in parallel uploading a CSV export from a Postgres database, say.

HTTP/1.1 has chunked encoding for that. As you know, that streams the _message_ itself, not the resource. This encoding is supported for "direct file uploads" in HTTPX, but not for `multipart/form-data`. This is strange, since chunked encoding is independent of `Content-Type`. Indeed, if both chunking and compression are in place, you first compress, and then you chunk the compressed bytes.

In HTTP/2 chunked encoding does not exist, byte size is per DATA frame. As you know, a message is allowed to have several. So, in this case the API would support uploading with multiple DATA frames, coming from a generator or iterator, for example.


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `httpx/_multipart.py`
Content:
```
1 import binascii
2 import io
3 import os
4 import typing
5 from pathlib import Path
6 
7 from ._types import (
8     AsyncByteStream,
9     FileContent,
10     FileTypes,
11     RequestData,
12     RequestFiles,
13     SyncByteStream,
14 )
15 from ._utils import (
16     format_form_param,
17     guess_content_type,
18     peek_filelike_length,
19     primitive_value_to_str,
20     to_bytes,
21 )
22 
23 
24 def get_multipart_boundary_from_content_type(
25     content_type: typing.Optional[bytes],
26 ) -> typing.Optional[bytes]:
27     if not content_type or not content_type.startswith(b"multipart/form-data"):
28         return None
29     # parse boundary according to
30     # https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1
31     if b";" in content_type:
32         for section in content_type.split(b";"):
33             if section.strip().lower().startswith(b"boundary="):
34                 return section.strip()[len(b"boundary=") :].strip(b'"')
35     return None
36 
37 
38 class DataField:
39     """
40     A single form field item, within a multipart form field.
41     """
42 
43     def __init__(
44         self, name: str, value: typing.Union[str, bytes, int, float, None]
45     ) -> None:
46         if not isinstance(name, str):
47             raise TypeError(
48                 f"Invalid type for name. Expected str, got {type(name)}: {name!r}"
49             )
50         if value is not None and not isinstance(value, (str, bytes, int, float)):
51             raise TypeError(
52                 f"Invalid type for value. Expected primitive type, got {type(value)}: {value!r}"
53             )
54         self.name = name
55         self.value: typing.Union[str, bytes] = (
56             value if isinstance(value, bytes) else primitive_value_to_str(value)
57         )
58 
59     def render_headers(self) -> bytes:
60         if not hasattr(self, "_headers"):
61             name = format_form_param("name", self.name)
62             self._headers = b"".join(
63                 [b"Content-Disposition: form-data; ", name, b"\r\n\r\n"]
64             )
65 
66         return self._headers
67 
68     def render_data(self) -> bytes:
69         if not hasattr(self, "_data"):
70             self._data = to_bytes(self.value)
71 
72         return self._data
73 
74     def get_length(self) -> int:
75         headers = self.render_headers()
76         data = self.render_data()
77         return len(headers) + len(data)
78 
79     def render(self) -> typing.Iterator[bytes]:
80         yield self.render_headers()
81         yield self.render_data()
82 
83 
84 class FileField:
85     """
86     A single file field item, within a multipart form field.
87     """
88 
89     CHUNK_SIZE = 64 * 1024
90 
91     def __init__(self, name: str, value: FileTypes) -> None:
92         self.name = name
93 
94         fileobj: FileContent
95 
96         headers: typing.Dict[str, str] = {}
97         content_type: typing.Optional[str] = None
98 
99         # This large tuple based API largely mirror's requests' API
100         # It would be good to think of better APIs for this that we could include in httpx 2.0
101         # since variable length tuples (especially of 4 elements) are quite unwieldly
102         if isinstance(value, tuple):
103             if len(value) == 2:
104                 # neither the 3rd parameter (content_type) nor the 4th (headers) was included
105                 filename, fileobj = value  # type: ignore
106             elif len(value) == 3:
107                 filename, fileobj, content_type = value  # type: ignore
108             else:
109                 # all 4 parameters included
110                 filename, fileobj, content_type, headers = value  # type: ignore
111         else:
112             filename = Path(str(getattr(value, "name", "upload"))).name
113             fileobj = value
114 
115         if content_type is None:
116             content_type = guess_content_type(filename)
117 
118         has_content_type_header = any("content-type" in key.lower() for key in headers)
119         if content_type is not None and not has_content_type_header:
120             # note that unlike requests, we ignore the content_type
121             # provided in the 3rd tuple element if it is also included in the headers
122             # requests does the opposite (it overwrites the header with the 3rd tuple element)
123             headers["Content-Type"] = content_type
124 
125         if "b" not in getattr(fileobj, "mode", "b"):
126             raise TypeError(
127                 "Multipart file uploads must be opened in binary mode, not text mode."
128             )
129         if isinstance(fileobj, io.StringIO):
130             raise TypeError(
131                 "Multipart file uploads require 'io.BytesIO', not 'io.StringIO'."
132             )
133 
134         self.filename = filename
135         self.file = fileobj
136         self.headers = headers
137 
138     def get_length(self) -> int:
139         headers = self.render_headers()
140 
141         if isinstance(self.file, (str, bytes)):
142             return len(headers) + len(to_bytes(self.file))
143 
144         # Let's do our best not to read `file` into memory.
145         file_length = peek_filelike_length(self.file)
146         if file_length is None:
147             # As a last resort, read file and cache contents for later.
148             assert not hasattr(self, "_data")
149             self._data = to_bytes(self.file.read())
150             file_length = len(self._data)
151 
152         return len(headers) + file_length
153 
154     def render_headers(self) -> bytes:
155         if not hasattr(self, "_headers"):
156             parts = [
157                 b"Content-Disposition: form-data; ",
158                 format_form_param("name", self.name),
159             ]
160             if self.filename:
161                 filename = format_form_param("filename", self.filename)
162                 parts.extend([b"; ", filename])
163             for header_name, header_value in self.headers.items():
164                 key, val = f"\r\n{header_name}: ".encode(), header_value.encode()
165                 parts.extend([key, val])
166             parts.append(b"\r\n\r\n")
167             self._headers = b"".join(parts)
168 
169         return self._headers
170 
171     def render_data(self) -> typing.Iterator[bytes]:
172         if isinstance(self.file, (str, bytes)):
173             yield to_bytes(self.file)
174             return
175 
176         if hasattr(self, "_data"):
177             # Already rendered.
178             yield self._data
179             return
180 
181         if hasattr(self.file, "seek"):
182             self.file.seek(0)
183 
184         chunk = self.file.read(self.CHUNK_SIZE)
185         while chunk:
186             yield to_bytes(chunk)
187             chunk = self.file.read(self.CHUNK_SIZE)
188 
189     def render(self) -> typing.Iterator[bytes]:
190         yield self.render_headers()
191         yield from self.render_data()
192 
193 
194 class MultipartStream(SyncByteStream, AsyncByteStream):
195     """
196     Request content as streaming multipart encoded form data.
197     """
198 
199     def __init__(
200         self,
201         data: RequestData,
202         files: RequestFiles,
203         boundary: typing.Optional[bytes] = None,
204     ) -> None:
205         if boundary is None:
206             boundary = binascii.hexlify(os.urandom(16))
207 
208         self.boundary = boundary
209         self.content_type = "multipart/form-data; boundary=%s" % boundary.decode(
210             "ascii"
211         )
212         self.fields = list(self._iter_fields(data, files))
213 
214     def _iter_fields(
215         self, data: RequestData, files: RequestFiles
216     ) -> typing.Iterator[typing.Union[FileField, DataField]]:
217         for name, value in data.items():
218             if isinstance(value, (tuple, list)):
219                 for item in value:
220                     yield DataField(name=name, value=item)
221             else:
222                 yield DataField(name=name, value=value)
223 
224         file_items = files.items() if isinstance(files, typing.Mapping) else files
225         for name, value in file_items:
226             yield FileField(name=name, value=value)
227 
228     def iter_chunks(self) -> typing.Iterator[bytes]:
229         for field in self.fields:
230             yield b"--%s\r\n" % self.boundary
231             yield from field.render()
232             yield b"\r\n"
233         yield b"--%s--\r\n" % self.boundary
234 
235     def iter_chunks_lengths(self) -> typing.Iterator[int]:
236         boundary_length = len(self.boundary)
237         # Follow closely what `.iter_chunks()` does.
238         for field in self.fields:
239             yield 2 + boundary_length + 2
240             yield field.get_length()
241             yield 2
242         yield 2 + boundary_length + 4
243 
244     def get_content_length(self) -> int:
245         return sum(self.iter_chunks_lengths())
246 
247     # Content stream interface.
248 
249     def get_headers(self) -> typing.Dict[str, str]:
250         content_length = str(self.get_content_length())
251         content_type = self.content_type
252         return {"Content-Length": content_length, "Content-Type": content_type}
253 
254     def __iter__(self) -> typing.Iterator[bytes]:
255         for chunk in self.iter_chunks():
256             yield chunk
257 
258     async def __aiter__(self) -> typing.AsyncIterator[bytes]:
259         for chunk in self.iter_chunks():
260             yield chunk
261
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/httpx/_multipart.py b/httpx/_multipart.py
--- a/httpx/_multipart.py
+++ b/httpx/_multipart.py
@@ -135,19 +135,18 @@
         self.file = fileobj
         self.headers = headers
 
-    def get_length(self) -> int:
+    def get_length(self) -> typing.Optional[int]:
         headers = self.render_headers()
 
         if isinstance(self.file, (str, bytes)):
             return len(headers) + len(to_bytes(self.file))
 
-        # Let's do our best not to read `file` into memory.
         file_length = peek_filelike_length(self.file)
+
+        # If we can't determine the filesize without reading it into memory,
+        # then return `None` here, to indicate an unknown file length.
         if file_length is None:
-            # As a last resort, read file and cache contents for later.
-            assert not hasattr(self, "_data")
-            self._data = to_bytes(self.file.read())
-            file_length = len(self._data)
+            return None
 
         return len(headers) + file_length
 
@@ -173,13 +172,11 @@
             yield to_bytes(self.file)
             return
 
-        if hasattr(self, "_data"):
-            # Already rendered.
-            yield self._data
-            return
-
         if hasattr(self.file, "seek"):
-            self.file.seek(0)
+            try:
+                self.file.seek(0)
+            except io.UnsupportedOperation:
+                pass
 
         chunk = self.file.read(self.CHUNK_SIZE)
         while chunk:
@@ -232,24 +229,34 @@
             yield b"\r\n"
         yield b"--%s--\r\n" % self.boundary
 
-    def iter_chunks_lengths(self) -> typing.Iterator[int]:
+    def get_content_length(self) -> typing.Optional[int]:
+        """
+        Return the length of the multipart encoded content, or `None` if
+        any of the files have a length that cannot be determined upfront.
+        """
         boundary_length = len(self.boundary)
-        # Follow closely what `.iter_chunks()` does.
+        length = 0
+
         for field in self.fields:
-            yield 2 + boundary_length + 2
-            yield field.get_length()
-            yield 2
-        yield 2 + boundary_length + 4
+            field_length = field.get_length()
+            if field_length is None:
+                return None
+
+            length += 2 + boundary_length + 2  # b"--{boundary}\r\n"
+            length += field_length
+            length += 2  # b"\r\n"
 
-    def get_content_length(self) -> int:
-        return sum(self.iter_chunks_lengths())
+        length += 2 + boundary_length + 4  # b"--{boundary}--\r\n"
+        return length
 
     # Content stream interface.
 
     def get_headers(self) -> typing.Dict[str, str]:
-        content_length = str(self.get_content_length())
+        content_length = self.get_content_length()
         content_type = self.content_type
-        return {"Content-Length": content_length, "Content-Type": content_type}
+        if content_length is None:
+            return {"Transfer-Encoding": "chunked", "Content-Type": content_type}
+        return {"Content-Length": str(content_length), "Content-Type": content_type}
 
     def __iter__(self) -> typing.Iterator[bytes]:
         for chunk in self.iter_chunks():

{"golden_diff": "diff --git a/httpx/_multipart.py b/httpx/_multipart.py\n--- a/httpx/_multipart.py\n+++ b/httpx/_multipart.py\n@@ -135,19 +135,18 @@\n         self.file = fileobj\n         self.headers = headers\n \n-    def get_length(self) -> int:\n+    def get_length(self) -> typing.Optional[int]:\n         headers = self.render_headers()\n \n         if isinstance(self.file, (str, bytes)):\n             return len(headers) + len(to_bytes(self.file))\n \n-        # Let's do our best not to read `file` into memory.\n         file_length = peek_filelike_length(self.file)\n+\n+        # If we can't determine the filesize without reading it into memory,\n+        # then return `None` here, to indicate an unknown file length.\n         if file_length is None:\n-            # As a last resort, read file and cache contents for later.\n-            assert not hasattr(self, \"_data\")\n-            self._data = to_bytes(self.file.read())\n-            file_length = len(self._data)\n+            return None\n \n         return len(headers) + file_length\n \n@@ -173,13 +172,11 @@\n             yield to_bytes(self.file)\n             return\n \n-        if hasattr(self, \"_data\"):\n-            # Already rendered.\n-            yield self._data\n-            return\n-\n         if hasattr(self.file, \"seek\"):\n-            self.file.seek(0)\n+            try:\n+                self.file.seek(0)\n+            except io.UnsupportedOperation:\n+                pass\n \n         chunk = self.file.read(self.CHUNK_SIZE)\n         while chunk:\n@@ -232,24 +229,34 @@\n             yield b\"\\r\\n\"\n         yield b\"--%s--\\r\\n\" % self.boundary\n \n-    def iter_chunks_lengths(self) -> typing.Iterator[int]:\n+    def get_content_length(self) -> typing.Optional[int]:\n+        \"\"\"\n+        Return the length of the multipart encoded content, or `None` if\n+        any of the files have a length that cannot be determined upfront.\n+        \"\"\"\n         boundary_length = len(self.boundary)\n-        # Follow closely what `.iter_chunks()` does.\n+        length = 0\n+\n         for field in self.fields:\n-            yield 2 + boundary_length + 2\n-            yield field.get_length()\n-            yield 2\n-        yield 2 + boundary_length + 4\n+            field_length = field.get_length()\n+            if field_length is None:\n+                return None\n+\n+            length += 2 + boundary_length + 2  # b\"--{boundary}\\r\\n\"\n+            length += field_length\n+            length += 2  # b\"\\r\\n\"\n \n-    def get_content_length(self) -> int:\n-        return sum(self.iter_chunks_lengths())\n+        length += 2 + boundary_length + 4  # b\"--{boundary}--\\r\\n\"\n+        return length\n \n     # Content stream interface.\n \n     def get_headers(self) -> typing.Dict[str, str]:\n-        content_length = str(self.get_content_length())\n+        content_length = self.get_content_length()\n         content_type = self.content_type\n-        return {\"Content-Length\": content_length, \"Content-Type\": content_type}\n+        if content_length is None:\n+            return {\"Transfer-Encoding\": \"chunked\", \"Content-Type\": content_type}\n+        return {\"Content-Length\": str(content_length), \"Content-Type\": content_type}\n \n     def __iter__(self) -> typing.Iterator[bytes]:\n         for chunk in self.iter_chunks():\n", "issue": "Add support for streaming multipart/form-data\n(After initial discussion in https://github.com/encode/httpx/discussions/2227.)\r\n\r\nAs far as I can see, there's no way to stream `multipart/form-data` uploads when the `Content-Length` is unknown, since HTTPX tries to get body size some way or another (last resort is to read it all in memory and count). Please, let me know if I overlooked something!\r\n\r\nUse case would be to stream something whose size is unknown beforehand, like fetching and in parallel uploading a CSV export from a Postgres database, say.\r\n\r\nHTTP/1.1 has chunked encoding for that. As you know, that streams the _message_ itself, not the resource. This encoding is supported for \"direct file uploads\" in HTTPX, but not for `multipart/form-data`. This is strange, since chunked encoding is independent of `Content-Type`. Indeed, if both chunking and compression are in place, you first compress, and then you chunk the compressed bytes.\r\n\r\nIn HTTP/2 chunked encoding does not exist, byte size is per DATA frame. As you know, a message is allowed to have several. So, in this case the API would support uploading with multiple DATA frames, coming from a generator or iterator, for example.\r\n\n", "before_files": [{"content": "import binascii\nimport io\nimport os\nimport typing\nfrom pathlib import Path\n\nfrom ._types import (\n    AsyncByteStream,\n    FileContent,\n    FileTypes,\n    RequestData,\n    RequestFiles,\n    SyncByteStream,\n)\nfrom ._utils import (\n    format_form_param,\n    guess_content_type,\n    peek_filelike_length,\n    primitive_value_to_str,\n    to_bytes,\n)\n\n\ndef get_multipart_boundary_from_content_type(\n    content_type: typing.Optional[bytes],\n) -> typing.Optional[bytes]:\n    if not content_type or not content_type.startswith(b\"multipart/form-data\"):\n        return None\n    # parse boundary according to\n    # https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1\n    if b\";\" in content_type:\n        for section in content_type.split(b\";\"):\n            if section.strip().lower().startswith(b\"boundary=\"):\n                return section.strip()[len(b\"boundary=\") :].strip(b'\"')\n    return None\n\n\nclass DataField:\n    \"\"\"\n    A single form field item, within a multipart form field.\n    \"\"\"\n\n    def __init__(\n        self, name: str, value: typing.Union[str, bytes, int, float, None]\n    ) -> None:\n        if not isinstance(name, str):\n            raise TypeError(\n                f\"Invalid type for name. Expected str, got {type(name)}: {name!r}\"\n            )\n        if value is not None and not isinstance(value, (str, bytes, int, float)):\n            raise TypeError(\n                f\"Invalid type for value. Expected primitive type, got {type(value)}: {value!r}\"\n            )\n        self.name = name\n        self.value: typing.Union[str, bytes] = (\n            value if isinstance(value, bytes) else primitive_value_to_str(value)\n        )\n\n    def render_headers(self) -> bytes:\n        if not hasattr(self, \"_headers\"):\n            name = format_form_param(\"name\", self.name)\n            self._headers = b\"\".join(\n                [b\"Content-Disposition: form-data; \", name, b\"\\r\\n\\r\\n\"]\n            )\n\n        return self._headers\n\n    def render_data(self) -> bytes:\n        if not hasattr(self, \"_data\"):\n            self._data = to_bytes(self.value)\n\n        return self._data\n\n    def get_length(self) -> int:\n        headers = self.render_headers()\n        data = self.render_data()\n        return len(headers) + len(data)\n\n    def render(self) -> typing.Iterator[bytes]:\n        yield self.render_headers()\n        yield self.render_data()\n\n\nclass FileField:\n    \"\"\"\n    A single file field item, within a multipart form field.\n    \"\"\"\n\n    CHUNK_SIZE = 64 * 1024\n\n    def __init__(self, name: str, value: FileTypes) -> None:\n        self.name = name\n\n        fileobj: FileContent\n\n        headers: typing.Dict[str, str] = {}\n        content_type: typing.Optional[str] = None\n\n        # This large tuple based API largely mirror's requests' API\n        # It would be good to think of better APIs for this that we could include in httpx 2.0\n        # since variable length tuples (especially of 4 elements) are quite unwieldly\n        if isinstance(value, tuple):\n            if len(value) == 2:\n                # neither the 3rd parameter (content_type) nor the 4th (headers) was included\n                filename, fileobj = value  # type: ignore\n            elif len(value) == 3:\n                filename, fileobj, content_type = value  # type: ignore\n            else:\n                # all 4 parameters included\n                filename, fileobj, content_type, headers = value  # type: ignore\n        else:\n            filename = Path(str(getattr(value, \"name\", \"upload\"))).name\n            fileobj = value\n\n        if content_type is None:\n            content_type = guess_content_type(filename)\n\n        has_content_type_header = any(\"content-type\" in key.lower() for key in headers)\n        if content_type is not None and not has_content_type_header:\n            # note that unlike requests, we ignore the content_type\n            # provided in the 3rd tuple element if it is also included in the headers\n            # requests does the opposite (it overwrites the header with the 3rd tuple element)\n            headers[\"Content-Type\"] = content_type\n\n        if \"b\" not in getattr(fileobj, \"mode\", \"b\"):\n            raise TypeError(\n                \"Multipart file uploads must be opened in binary mode, not text mode.\"\n            )\n        if isinstance(fileobj, io.StringIO):\n            raise TypeError(\n                \"Multipart file uploads require 'io.BytesIO', not 'io.StringIO'.\"\n            )\n\n        self.filename = filename\n        self.file = fileobj\n        self.headers = headers\n\n    def get_length(self) -> int:\n        headers = self.render_headers()\n\n        if isinstance(self.file, (str, bytes)):\n            return len(headers) + len(to_bytes(self.file))\n\n        # Let's do our best not to read `file` into memory.\n        file_length = peek_filelike_length(self.file)\n        if file_length is None:\n            # As a last resort, read file and cache contents for later.\n            assert not hasattr(self, \"_data\")\n            self._data = to_bytes(self.file.read())\n            file_length = len(self._data)\n\n        return len(headers) + file_length\n\n    def render_headers(self) -> bytes:\n        if not hasattr(self, \"_headers\"):\n            parts = [\n                b\"Content-Disposition: form-data; \",\n                format_form_param(\"name\", self.name),\n            ]\n            if self.filename:\n                filename = format_form_param(\"filename\", self.filename)\n                parts.extend([b\"; \", filename])\n            for header_name, header_value in self.headers.items():\n                key, val = f\"\\r\\n{header_name}: \".encode(), header_value.encode()\n                parts.extend([key, val])\n            parts.append(b\"\\r\\n\\r\\n\")\n            self._headers = b\"\".join(parts)\n\n        return self._headers\n\n    def render_data(self) -> typing.Iterator[bytes]:\n        if isinstance(self.file, (str, bytes)):\n            yield to_bytes(self.file)\n            return\n\n        if hasattr(self, \"_data\"):\n            # Already rendered.\n            yield self._data\n            return\n\n        if hasattr(self.file, \"seek\"):\n            self.file.seek(0)\n\n        chunk = self.file.read(self.CHUNK_SIZE)\n        while chunk:\n            yield to_bytes(chunk)\n            chunk = self.file.read(self.CHUNK_SIZE)\n\n    def render(self) -> typing.Iterator[bytes]:\n        yield self.render_headers()\n        yield from self.render_data()\n\n\nclass MultipartStream(SyncByteStream, AsyncByteStream):\n    \"\"\"\n    Request content as streaming multipart encoded form data.\n    \"\"\"\n\n    def __init__(\n        self,\n        data: RequestData,\n        files: RequestFiles,\n        boundary: typing.Optional[bytes] = None,\n    ) -> None:\n        if boundary is None:\n            boundary = binascii.hexlify(os.urandom(16))\n\n        self.boundary = boundary\n        self.content_type = \"multipart/form-data; boundary=%s\" % boundary.decode(\n            \"ascii\"\n        )\n        self.fields = list(self._iter_fields(data, files))\n\n    def _iter_fields(\n        self, data: RequestData, files: RequestFiles\n    ) -> typing.Iterator[typing.Union[FileField, DataField]]:\n        for name, value in data.items():\n            if isinstance(value, (tuple, list)):\n                for item in value:\n                    yield DataField(name=name, value=item)\n            else:\n                yield DataField(name=name, value=value)\n\n        file_items = files.items() if isinstance(files, typing.Mapping) else files\n        for name, value in file_items:\n            yield FileField(name=name, value=value)\n\n    def iter_chunks(self) -> typing.Iterator[bytes]:\n        for field in self.fields:\n            yield b\"--%s\\r\\n\" % self.boundary\n            yield from field.render()\n            yield b\"\\r\\n\"\n        yield b\"--%s--\\r\\n\" % self.boundary\n\n    def iter_chunks_lengths(self) -> typing.Iterator[int]:\n        boundary_length = len(self.boundary)\n        # Follow closely what `.iter_chunks()` does.\n        for field in self.fields:\n            yield 2 + boundary_length + 2\n            yield field.get_length()\n            yield 2\n        yield 2 + boundary_length + 4\n\n    def get_content_length(self) -> int:\n        return sum(self.iter_chunks_lengths())\n\n    # Content stream interface.\n\n    def get_headers(self) -> typing.Dict[str, str]:\n        content_length = str(self.get_content_length())\n        content_type = self.content_type\n        return {\"Content-Length\": content_length, \"Content-Type\": content_type}\n\n    def __iter__(self) -> typing.Iterator[bytes]:\n        for chunk in self.iter_chunks():\n            yield chunk\n\n    async def __aiter__(self) -> typing.AsyncIterator[bytes]:\n        for chunk in self.iter_chunks():\n            yield chunk\n", "path": "httpx/_multipart.py"}], "after_files": [{"content": "import binascii\nimport io\nimport os\nimport typing\nfrom pathlib import Path\n\nfrom ._types import (\n    AsyncByteStream,\n    FileContent,\n    FileTypes,\n    RequestData,\n    RequestFiles,\n    SyncByteStream,\n)\nfrom ._utils import (\n    format_form_param,\n    guess_content_type,\n    peek_filelike_length,\n    primitive_value_to_str,\n    to_bytes,\n)\n\n\ndef get_multipart_boundary_from_content_type(\n    content_type: typing.Optional[bytes],\n) -> typing.Optional[bytes]:\n    if not content_type or not content_type.startswith(b\"multipart/form-data\"):\n        return None\n    # parse boundary according to\n    # https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1\n    if b\";\" in content_type:\n        for section in content_type.split(b\";\"):\n            if section.strip().lower().startswith(b\"boundary=\"):\n                return section.strip()[len(b\"boundary=\") :].strip(b'\"')\n    return None\n\n\nclass DataField:\n    \"\"\"\n    A single form field item, within a multipart form field.\n    \"\"\"\n\n    def __init__(\n        self, name: str, value: typing.Union[str, bytes, int, float, None]\n    ) -> None:\n        if not isinstance(name, str):\n            raise TypeError(\n                f\"Invalid type for name. Expected str, got {type(name)}: {name!r}\"\n            )\n        if value is not None and not isinstance(value, (str, bytes, int, float)):\n            raise TypeError(\n                f\"Invalid type for value. Expected primitive type, got {type(value)}: {value!r}\"\n            )\n        self.name = name\n        self.value: typing.Union[str, bytes] = (\n            value if isinstance(value, bytes) else primitive_value_to_str(value)\n        )\n\n    def render_headers(self) -> bytes:\n        if not hasattr(self, \"_headers\"):\n            name = format_form_param(\"name\", self.name)\n            self._headers = b\"\".join(\n                [b\"Content-Disposition: form-data; \", name, b\"\\r\\n\\r\\n\"]\n            )\n\n        return self._headers\n\n    def render_data(self) -> bytes:\n        if not hasattr(self, \"_data\"):\n            self._data = to_bytes(self.value)\n\n        return self._data\n\n    def get_length(self) -> int:\n        headers = self.render_headers()\n        data = self.render_data()\n        return len(headers) + len(data)\n\n    def render(self) -> typing.Iterator[bytes]:\n        yield self.render_headers()\n        yield self.render_data()\n\n\nclass FileField:\n    \"\"\"\n    A single file field item, within a multipart form field.\n    \"\"\"\n\n    CHUNK_SIZE = 64 * 1024\n\n    def __init__(self, name: str, value: FileTypes) -> None:\n        self.name = name\n\n        fileobj: FileContent\n\n        headers: typing.Dict[str, str] = {}\n        content_type: typing.Optional[str] = None\n\n        # This large tuple based API largely mirror's requests' API\n        # It would be good to think of better APIs for this that we could include in httpx 2.0\n        # since variable length tuples (especially of 4 elements) are quite unwieldly\n        if isinstance(value, tuple):\n            if len(value) == 2:\n                # neither the 3rd parameter (content_type) nor the 4th (headers) was included\n                filename, fileobj = value  # type: ignore\n            elif len(value) == 3:\n                filename, fileobj, content_type = value  # type: ignore\n            else:\n                # all 4 parameters included\n                filename, fileobj, content_type, headers = value  # type: ignore\n        else:\n            filename = Path(str(getattr(value, \"name\", \"upload\"))).name\n            fileobj = value\n\n        if content_type is None:\n            content_type = guess_content_type(filename)\n\n        has_content_type_header = any(\"content-type\" in key.lower() for key in headers)\n        if content_type is not None and not has_content_type_header:\n            # note that unlike requests, we ignore the content_type\n            # provided in the 3rd tuple element if it is also included in the headers\n            # requests does the opposite (it overwrites the header with the 3rd tuple element)\n            headers[\"Content-Type\"] = content_type\n\n        if \"b\" not in getattr(fileobj, \"mode\", \"b\"):\n            raise TypeError(\n                \"Multipart file uploads must be opened in binary mode, not text mode.\"\n            )\n        if isinstance(fileobj, io.StringIO):\n            raise TypeError(\n                \"Multipart file uploads require 'io.BytesIO', not 'io.StringIO'.\"\n            )\n\n        self.filename = filename\n        self.file = fileobj\n        self.headers = headers\n\n    def get_length(self) -> typing.Optional[int]:\n        headers = self.render_headers()\n\n        if isinstance(self.file, (str, bytes)):\n            return len(headers) + len(to_bytes(self.file))\n\n        file_length = peek_filelike_length(self.file)\n\n        # If we can't determine the filesize without reading it into memory,\n        # then return `None` here, to indicate an unknown file length.\n        if file_length is None:\n            return None\n\n        return len(headers) + file_length\n\n    def render_headers(self) -> bytes:\n        if not hasattr(self, \"_headers\"):\n            parts = [\n                b\"Content-Disposition: form-data; \",\n                format_form_param(\"name\", self.name),\n            ]\n            if self.filename:\n                filename = format_form_param(\"filename\", self.filename)\n                parts.extend([b\"; \", filename])\n            for header_name, header_value in self.headers.items():\n                key, val = f\"\\r\\n{header_name}: \".encode(), header_value.encode()\n                parts.extend([key, val])\n            parts.append(b\"\\r\\n\\r\\n\")\n            self._headers = b\"\".join(parts)\n\n        return self._headers\n\n    def render_data(self) -> typing.Iterator[bytes]:\n        if isinstance(self.file, (str, bytes)):\n            yield to_bytes(self.file)\n            return\n\n        if hasattr(self.file, \"seek\"):\n            try:\n                self.file.seek(0)\n            except io.UnsupportedOperation:\n                pass\n\n        chunk = self.file.read(self.CHUNK_SIZE)\n        while chunk:\n            yield to_bytes(chunk)\n            chunk = self.file.read(self.CHUNK_SIZE)\n\n    def render(self) -> typing.Iterator[bytes]:\n        yield self.render_headers()\n        yield from self.render_data()\n\n\nclass MultipartStream(SyncByteStream, AsyncByteStream):\n    \"\"\"\n    Request content as streaming multipart encoded form data.\n    \"\"\"\n\n    def __init__(\n        self,\n        data: RequestData,\n        files: RequestFiles,\n        boundary: typing.Optional[bytes] = None,\n    ) -> None:\n        if boundary is None:\n            boundary = binascii.hexlify(os.urandom(16))\n\n        self.boundary = boundary\n        self.content_type = \"multipart/form-data; boundary=%s\" % boundary.decode(\n            \"ascii\"\n        )\n        self.fields = list(self._iter_fields(data, files))\n\n    def _iter_fields(\n        self, data: RequestData, files: RequestFiles\n    ) -> typing.Iterator[typing.Union[FileField, DataField]]:\n        for name, value in data.items():\n            if isinstance(value, (tuple, list)):\n                for item in value:\n                    yield DataField(name=name, value=item)\n            else:\n                yield DataField(name=name, value=value)\n\n        file_items = files.items() if isinstance(files, typing.Mapping) else files\n        for name, value in file_items:\n            yield FileField(name=name, value=value)\n\n    def iter_chunks(self) -> typing.Iterator[bytes]:\n        for field in self.fields:\n            yield b\"--%s\\r\\n\" % self.boundary\n            yield from field.render()\n            yield b\"\\r\\n\"\n        yield b\"--%s--\\r\\n\" % self.boundary\n\n    def get_content_length(self) -> typing.Optional[int]:\n        \"\"\"\n        Return the length of the multipart encoded content, or `None` if\n        any of the files have a length that cannot be determined upfront.\n        \"\"\"\n        boundary_length = len(self.boundary)\n        length = 0\n\n        for field in self.fields:\n            field_length = field.get_length()\n            if field_length is None:\n                return None\n\n            length += 2 + boundary_length + 2  # b\"--{boundary}\\r\\n\"\n            length += field_length\n            length += 2  # b\"\\r\\n\"\n\n        length += 2 + boundary_length + 4  # b\"--{boundary}--\\r\\n\"\n        return length\n\n    # Content stream interface.\n\n    def get_headers(self) -> typing.Dict[str, str]:\n        content_length = self.get_content_length()\n        content_type = self.content_type\n        if content_length is None:\n            return {\"Transfer-Encoding\": \"chunked\", \"Content-Type\": content_type}\n        return {\"Content-Length\": str(content_length), \"Content-Type\": content_type}\n\n    def __iter__(self) -> typing.Iterator[bytes]:\n        for chunk in self.iter_chunks():\n            yield chunk\n\n    async def __aiter__(self) -> typing.AsyncIterator[bytes]:\n        for chunk in self.iter_chunks():\n            yield chunk\n", "path": "httpx/_multipart.py"}]}