]> git.ipfire.org Git - thirdparty/httpx.git/commitdiff
Allow str content for multipart upload files (#2400)
authorTom Christie <tom@tomchristie.com>
Thu, 6 Oct 2022 16:53:51 +0000 (17:53 +0100)
committerGitHub <noreply@github.com>
Thu, 6 Oct 2022 16:53:51 +0000 (17:53 +0100)
httpx/_multipart.py
httpx/_types.py
tests/test_multipart.py

index 0329649758569c919470b1dbfab320204fafed1c..2c08776f4b956c1ad245bbca3d5ef8f23ef1c49f 100644 (file)
@@ -122,8 +122,14 @@ class FileField:
             # requests does the opposite (it overwrites the header with the 3rd tuple element)
             headers["Content-Type"] = content_type
 
-        if isinstance(fileobj, (str, io.StringIO)):
-            raise TypeError(f"Expected bytes or bytes-like object got: {type(fileobj)}")
+        if "b" not in getattr(fileobj, "mode", "b"):
+            raise TypeError(
+                "Multipart file uploads must be opened in binary mode, not text mode."
+            )
+        if isinstance(fileobj, io.StringIO):
+            raise TypeError(
+                "Multipart file uploads require 'io.BytesIO', not 'io.StringIO'."
+            )
 
         self.filename = filename
         self.file = fileobj
index e015844bbfeb571803b13790862d6dde9e328c5c..8099f7b4dd8de1dc4e0e036c55d2f4fa46d1c35f 100644 (file)
@@ -80,7 +80,7 @@ ResponseExtensions = Dict[str, Any]
 
 RequestData = Mapping[str, Any]
 
-FileContent = Union[IO[bytes], bytes]
+FileContent = Union[IO[bytes], bytes, str]
 FileTypes = Union[
     # file (or bytes)
     FileContent,
index dc93d26505f7a767eaa855ff920cb879e9f6e09b..a4e9796bd740d00166deed648129be61b07be88a 100644 (file)
@@ -339,18 +339,37 @@ def test_multipart_encode_files_allows_bytes_content() -> None:
         assert content == b"".join(stream)
 
 
-def test_multipart_encode_files_raises_exception_with_str_content() -> None:
-    files = {"file": ("test.txt", "<bytes content>", "text/plain")}
+def test_multipart_encode_files_allows_str_content() -> None:
+    files = {"file": ("test.txt", "<str content>", "text/plain")}
     with mock.patch("os.urandom", return_value=os.urandom(16)):
+        boundary = os.urandom(16).hex()
 
-        with pytest.raises(TypeError):
-            encode_request(data={}, files=files)  # type: ignore
+        headers, stream = encode_request(data={}, files=files)
+        assert isinstance(stream, typing.Iterable)
+
+        content = (
+            '--{0}\r\nContent-Disposition: form-data; name="file"; '
+            'filename="test.txt"\r\n'
+            "Content-Type: text/plain\r\n\r\n<str content>\r\n"
+            "--{0}--\r\n"
+            "".format(boundary).encode("ascii")
+        )
+        assert headers == {
+            "Content-Type": f"multipart/form-data; boundary={boundary}",
+            "Content-Length": str(len(content)),
+        }
+        assert content == b"".join(stream)
 
 
 def test_multipart_encode_files_raises_exception_with_StringIO_content() -> None:
     files = {"file": ("test.txt", io.StringIO("content"), "text/plain")}
-    with mock.patch("os.urandom", return_value=os.urandom(16)):
+    with pytest.raises(TypeError):
+        encode_request(data={}, files=files)  # type: ignore
+
 
+def test_multipart_encode_files_raises_exception_with_text_mode_file() -> None:
+    with tempfile.TemporaryFile(mode="w") as upload:
+        files = {"file": ("test.txt", upload, "text/plain")}
         with pytest.raises(TypeError):
             encode_request(data={}, files=files)  # type: ignore