+3
-2
backend/src/backend/storage/r2.py
+3
-2
backend/src/backend/storage/r2.py
···
2
2
3
3
import time
4
4
from collections.abc import Callable
5
+
from io import BytesIO
5
6
from pathlib import Path
6
7
from typing import BinaryIO
7
8
···
120
121
121
122
async def save(
122
123
self,
123
-
file: BinaryIO,
124
+
file: BinaryIO | BytesIO,
124
125
filename: str,
125
126
progress_callback: Callable[[float], None] | None = None,
126
127
) -> str:
···
444
445
445
446
async def save_gated(
446
447
self,
447
-
file: BinaryIO,
448
+
file: BinaryIO | BytesIO,
448
449
filename: str,
449
450
progress_callback: Callable[[float], None] | None = None,
450
451
) -> str:
+53
backend/tests/test_storage_types.py
+53
backend/tests/test_storage_types.py
···
1
+
"""test storage type hints accept BytesIO.
2
+
3
+
regression test for: https://github.com/zzstoatzz/plyr.fm/pull/736
4
+
beartype was rejecting BytesIO for BinaryIO type hint in R2Storage.save()
5
+
"""
6
+
7
+
from io import BytesIO
8
+
from unittest.mock import AsyncMock, patch
9
+
10
+
from backend.storage.r2 import R2Storage
11
+
12
+
13
+
async def test_r2_save_accepts_bytesio():
14
+
"""R2Storage.save() should accept BytesIO objects.
15
+
16
+
BytesIO is the standard way to create in-memory binary streams,
17
+
and is used throughout the codebase for image uploads.
18
+
19
+
This test verifies that the type hint on save() is compatible
20
+
with BytesIO, which beartype validates at runtime.
21
+
"""
22
+
# create a minimal image-like BytesIO
23
+
image_data = b"\x89PNG\r\n\x1a\n" + b"\x00" * 100 # fake PNG header
24
+
file_obj = BytesIO(image_data)
25
+
26
+
# mock the R2 client internals
27
+
with (
28
+
patch.object(R2Storage, "__init__", lambda self: None),
29
+
patch("backend.storage.r2.hash_file_chunked", return_value="abc123def456"),
30
+
):
31
+
storage = R2Storage()
32
+
storage.async_session = AsyncMock()
33
+
storage.image_bucket_name = "test-images"
34
+
storage.audio_bucket_name = "test-audio"
35
+
36
+
# mock the async context manager for S3 client
37
+
mock_client = AsyncMock()
38
+
mock_client.upload_fileobj = AsyncMock()
39
+
40
+
mock_cm = AsyncMock()
41
+
mock_cm.__aenter__ = AsyncMock(return_value=mock_client)
42
+
mock_cm.__aexit__ = AsyncMock(return_value=None)
43
+
storage.async_session.client = lambda *args, **kwargs: mock_cm
44
+
storage.endpoint_url = "https://test.r2.dev"
45
+
storage.aws_access_key_id = "test"
46
+
storage.aws_secret_access_key = "test"
47
+
48
+
# this should NOT raise a beartype error
49
+
# before the fix: BeartypeCallHintParamViolation
50
+
file_id = await storage.save(file_obj, "test.png")
51
+
52
+
assert file_id == "abc123def456"[:16]
53
+
mock_client.upload_fileobj.assert_called_once()