Skip to content

Commit c4f35a2

Browse files
fix: preserve memoryview bytes in sandbox streaming uploads
1 parent 1a9eb35 commit c4f35a2

3 files changed

Lines changed: 82 additions & 2 deletions

File tree

‎hyperbrowser/client/managers/async_manager/sandboxes/sandbox_files.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ async def _aiter_stream_content(stream, chunk_size: int = DEFAULT_TRANSFER_CHUNK
6464
if isinstance(stream, str):
6565
yield stream.encode("utf-8")
6666
return
67-
if isinstance(stream, (bytes, bytearray)):
67+
if isinstance(stream, (bytes, bytearray, memoryview)):
6868
yield bytes(stream)
6969
return
7070
if hasattr(stream, "__aiter__"):
@@ -492,6 +492,7 @@ async def upload_stream(
492492
content_length: Optional[int] = None,
493493
chunk_size: int = DEFAULT_TRANSFER_CHUNK_SIZE,
494494
):
495+
"""Upload a bytes-like buffer, readable file, or iterable of byte chunks."""
495496
headers = {}
496497
if content_length is not None:
497498
headers["content-length"] = str(content_length)

‎hyperbrowser/client/managers/sync_manager/sandboxes/sandbox_files.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def _iter_stream_content(stream, chunk_size: int = DEFAULT_TRANSFER_CHUNK_SIZE):
5757
if isinstance(stream, str):
5858
yield stream.encode("utf-8")
5959
return
60-
if isinstance(stream, (bytes, bytearray)):
60+
if isinstance(stream, (bytes, bytearray, memoryview)):
6161
yield bytes(stream)
6262
return
6363
if hasattr(stream, "read"):
@@ -461,6 +461,7 @@ def upload_stream(
461461
content_length: Optional[int] = None,
462462
chunk_size: int = DEFAULT_TRANSFER_CHUNK_SIZE,
463463
):
464+
"""Upload a bytes-like buffer, readable file, or iterable of byte chunks."""
464465
headers = {}
465466
if content_length is not None:
466467
headers["content-length"] = str(content_length)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Exercise buffer inputs through the public sandbox file upload methods."""
2+
3+
import io
4+
import struct
5+
from array import array
6+
7+
import pytest
8+
9+
from hyperbrowser.client.managers.async_manager.sandboxes.sandbox_files import (
10+
SandboxFilesApi as AsyncSandboxFilesApi,
11+
)
12+
from hyperbrowser.client.managers.sync_manager.sandboxes.sandbox_files import (
13+
SandboxFilesApi,
14+
)
15+
16+
17+
CASES = [
18+
(lambda: memoryview(b"\x00\xffab"), b"\x00\xffab"),
19+
(lambda: memoryview(b"prefixpayloadsuffix")[6:13], b"payload"),
20+
(lambda: memoryview(b"abcdef")[::2], b"ace"),
21+
(
22+
lambda: memoryview(array("H", [0x0102, 0x0304])),
23+
struct.pack("=HH", 0x0102, 0x0304),
24+
),
25+
(lambda: memoryview(b""), b""),
26+
(lambda: b"abc", b"abc"),
27+
(lambda: bytearray(b"abc"), b"abc"),
28+
(lambda: io.BytesIO(b"abcdef"), b"abcdef"),
29+
(lambda: iter([b"ab", b"cd", b"ef"]), b"abcdef"),
30+
]
31+
32+
33+
class RecordingTransport:
34+
def request_json(self, path, *, content, params, method, headers):
35+
self.body = b"".join(content)
36+
self.path = path
37+
self.params = params
38+
self.method = method
39+
self.headers = headers
40+
return {"path": params["path"], "bytesWritten": len(self.body)}
41+
42+
43+
class AsyncRecordingTransport(RecordingTransport):
44+
async def request_json(self, path, *, content, params, method, headers):
45+
chunks = [chunk async for chunk in content]
46+
return super().request_json(
47+
path,
48+
content=chunks,
49+
params=params,
50+
method=method,
51+
headers=headers,
52+
)
53+
54+
55+
@pytest.mark.parametrize("make_stream,expected", CASES)
56+
@pytest.mark.parametrize("asynchronous", [False, True])
57+
@pytest.mark.anyio
58+
async def test_upload_stream_preserves_buffer_bytes(
59+
make_stream, expected, asynchronous
60+
):
61+
stream = make_stream()
62+
if asynchronous:
63+
transport = AsyncRecordingTransport()
64+
files = AsyncSandboxFilesApi(transport, lambda: None)
65+
result = await files.upload_stream("/tmp/buffer.bin", stream, chunk_size=2)
66+
else:
67+
transport = RecordingTransport()
68+
files = SandboxFilesApi(transport, lambda: None)
69+
result = files.upload_stream("/tmp/buffer.bin", stream, chunk_size=2)
70+
71+
assert transport.body == expected
72+
assert result.bytes_written == len(expected)
73+
assert result.path == "/tmp/buffer.bin"
74+
assert transport.path == "/sandbox/files/upload"
75+
assert transport.method == "PUT"
76+
assert transport.params == {"path": "/tmp/buffer.bin"}
77+
if isinstance(stream, memoryview):
78+
assert stream.tobytes() == expected # The caller's view remains usable.

0 commit comments

Comments
 (0)