From 701613ff2a81df5b9d8ace889c37fec105d5d093 Mon Sep 17 00:00:00 2001 From: Anton Soloviev Date: Sun, 11 Oct 2026 21:40:35 -0700 Subject: [PATCH 1/2] Fall back to read when file adapters leave readinto unimplemented --- soundfile.py | 2 +- tests/test_read_fallback.py | 61 +++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/test_read_fallback.py diff --git a/soundfile.py b/soundfile.py index 0bf3a6e..a3984a3 100644 --- a/soundfile.py +++ b/soundfile.py @@ -1325,7 +1325,7 @@ def vio_read(ptr, count, user_data): try: buf = _ffi.buffer(ptr, count) data_read = file.readinto(buf) - except AttributeError: + except (AttributeError, NotImplementedError): data = file.read(count) data_read = len(data) buf = _ffi.buffer(ptr, data_read) diff --git a/tests/test_read_fallback.py b/tests/test_read_fallback.py new file mode 100644 index 0000000..8988eee --- /dev/null +++ b/tests/test_read_fallback.py @@ -0,0 +1,61 @@ +import io + +import numpy as np +import pytest +from numpy.testing import assert_array_equal + +import soundfile as sf + + +class ReadAdapter(io.RawIOBase): + def __init__(self, contents): + self.source = io.BytesIO(contents) + self.read_calls = 0 + + def read(self, size=-1): + self.read_calls += 1 + return self.source.read(size) + + def seek(self, offset, whence=0): + return self.source.seek(offset, whence) + + def tell(self): + return self.source.tell() + + def readable(self): + return True + + def seekable(self): + return True + + +@pytest.mark.parametrize('format', ['WAV', 'FLAC']) +@pytest.mark.parametrize('channels', [1, 2]) +def test_virtual_io_read_adapter_with_unimplemented_readinto(format, channels): + values = np.array([1, 256, -2345, 42], dtype='int16') + if channels == 2: + values = np.column_stack((values, -values)) + encoded = io.BytesIO() + sf.write(encoded, values, 8000, format=format, subtype='PCM_16') + adapter = ReadAdapter(encoded.getvalue()) + # The inherited method exists, but is not implemented by this adapter. + with pytest.raises(NotImplementedError): + adapter.readinto(bytearray(1)) + decoded, rate = sf.read(adapter, dtype='int16') + assert rate == 8000 + assert_array_equal(decoded, values) + assert adapter.read_calls > 0 + + +def test_virtual_io_prefers_working_readinto(): + class ReadIntoAdapter(ReadAdapter): + def readinto(self, buffer): + return self.source.readinto(buffer) + + values = np.array([1, 256, -2345, 42], dtype='int16') + encoded = io.BytesIO() + sf.write(encoded, values, 8000, format='WAV', subtype='PCM_16') + adapter = ReadIntoAdapter(encoded.getvalue()) + decoded, _ = sf.read(adapter, dtype='int16') + assert_array_equal(decoded, values) + assert adapter.read_calls == 0 From ad2316d4022f2c8b32d76e2314cdda8358b30c07 Mon Sep 17 00:00:00 2001 From: Anton Soloviev Date: Mon, 12 Oct 2026 12:17:48 -0700 Subject: [PATCH 2/2] Make unimplemented readinto regression portable to PyPy --- tests/test_read_fallback.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_read_fallback.py b/tests/test_read_fallback.py index 8988eee..ce2fcc3 100644 --- a/tests/test_read_fallback.py +++ b/tests/test_read_fallback.py @@ -12,6 +12,10 @@ def __init__(self, contents): self.source = io.BytesIO(contents) self.read_calls = 0 + def readinto(self, buffer): + # PyPy does not expose RawIOBase's stub; exercise the same capability failure. + raise NotImplementedError + def read(self, size=-1): self.read_calls += 1 return self.source.read(size) @@ -38,7 +42,7 @@ def test_virtual_io_read_adapter_with_unimplemented_readinto(format, channels): encoded = io.BytesIO() sf.write(encoded, values, 8000, format=format, subtype='PCM_16') adapter = ReadAdapter(encoded.getvalue()) - # The inherited method exists, but is not implemented by this adapter. + # The method exists, but is deliberately not implemented by this adapter. with pytest.raises(NotImplementedError): adapter.readinto(bytearray(1)) decoded, rate = sf.read(adapter, dtype='int16')