from ._multipart import MultipartStream
from ._transports.base import AsyncByteStream, SyncByteStream
from ._types import RequestContent, RequestData, RequestFiles, ResponseContent
-from ._utils import primitive_value_to_str
+from ._utils import peek_filelike_length, primitive_value_to_str
class ByteStream(AsyncByteStream, SyncByteStream):
if isinstance(content, (bytes, str)):
body = content.encode("utf-8") if isinstance(content, str) else content
- content_length = str(len(body))
- headers = {"Content-Length": content_length} if body else {}
+ content_length = len(body)
+ headers = {"Content-Length": str(content_length)} if body else {}
return headers, ByteStream(body)
elif isinstance(content, Iterable):
- headers = {"Transfer-Encoding": "chunked"}
+ content_length_or_none = peek_filelike_length(content)
+
+ if content_length_or_none is None:
+ headers = {"Transfer-Encoding": "chunked"}
+ else:
+ headers = {"Content-Length": str(content_length_or_none)}
return headers, IteratorByteStream(content) # type: ignore
elif isinstance(content, AsyncIterable):
return len(headers) + len(to_bytes(self.file))
# Let's do our best not to read `file` into memory.
- try:
- file_length = peek_filelike_length(self.file)
- except OSError:
+ file_length = peek_filelike_length(self.file)
+ 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())
return None
-def peek_filelike_length(stream: typing.IO) -> int:
+def peek_filelike_length(stream: typing.Any) -> typing.Optional[int]:
"""
Given a file-like stream object, return its length in number of bytes
without reading it into memory.
try:
# Is it an actual file?
fd = stream.fileno()
- except OSError:
+ # Yup, seems to be an actual file.
+ length = os.fstat(fd).st_size
+ except (AttributeError, OSError):
# No... Maybe it's something that supports random access, like `io.BytesIO`?
try:
# Assuming so, go to end of stream to figure out its length,
offset = stream.tell()
length = stream.seek(0, os.SEEK_END)
stream.seek(offset)
- except OSError:
+ except (AttributeError, OSError):
# Not even that? Sorry, we're doomed...
- raise
- else:
- return length
- else:
- # Yup, seems to be an actual file.
- return os.fstat(fd).st_size
+ return None
+
+ return length
class Timer:
assert async_content == b"Hello, world!"
+@pytest.mark.asyncio
+async def test_bytesio_content():
+ headers, stream = encode_request(content=io.BytesIO(b"Hello, world!"))
+ assert isinstance(stream, typing.Iterable)
+ assert not isinstance(stream, typing.AsyncIterable)
+
+ content = b"".join([part for part in stream])
+
+ assert headers == {"Content-Length": "13"}
+ assert content == b"Hello, world!"
+
+
@pytest.mark.asyncio
async def test_iterator_content():
def hello_world():