Merge pull request #736 from zzstoatzz/fix/bytesio-type-hint

fix: accept BytesIO in R2Storage.save() type hint

authored by zzstoatzz.io and committed by GitHub b637ce46 b7d02446

Changed files
+56 -2
backend
src
backend
storage
tests
+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
··· 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()