From 1998fea31c77c33d9faab355c1622555c2445ed0 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 6 Aug 2014 13:40:28 +0200 Subject: [PATCH 1/7] Change 'r'/'w'/'rw' to 'r'/'w'/'r+'/'w+'/'x'/'x+' --- pysoundfile.py | 165 ++++++++++++++++---------------- tests/test_pysoundfile.py | 191 ++++++++++++++++++++++++++------------ 2 files changed, 219 insertions(+), 137 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 8452b57..d8c3211 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -2,6 +2,11 @@ from cffi import FFI as _FFI from os import SEEK_SET, SEEK_CUR, SEEK_END +try: + import builtins as _builtins +except ImportError: + import __builtin__ as _builtins # for Python < 3.0 + __version__ = "0.5.0" """PySoundFile is an audio library based on libsndfile, CFFI and Numpy @@ -65,6 +70,11 @@ { SF_FALSE = 0, SF_TRUE = 1, + + /* Modes for opening files. */ + SFM_READ = 0x10, + SFM_WRITE = 0x20, + SFM_RDWR = 0x30, } ; typedef int64_t sf_count_t ; @@ -149,12 +159,6 @@ } SF_FORMAT_INFO ; """) -_open_modes = { - 'r': 0x10, - 'w': 0x20, - 'rw': 0x30, -} - _str_types = { 'title': 0x01, 'copyright': 0x02, @@ -274,8 +278,8 @@ class SoundFile(object): Each SoundFile opens one sound file on the disk. This sound file has a specific samplerate, data format and a set number of - channels. Each sound file can be opened with one of the modes - 'r'/'w'/'rw'. Note that 'rw' is unsupported for some formats. + channels. Each sound file can be opened for reading, for writing or + both. Note that the latter is unsupported for some formats. Data can be written to the file using write(), or read from the file using read(). Every read and write operation starts at a @@ -307,9 +311,9 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, subtype=None, endian=None, format=None, closefd=True): """Open a sound file. - If a file is opened with mode 'r' (the default) or 'rw', + If a file is opened with mode 'r' (the default) or 'r+', no samplerate, channels or file format need to be given. If a - file is opened with mode 'w', you must provide a samplerate, + file is opened with another mode, you must provide a samplerate, a number of channels, and a file format. An exception is the RAW data format, which requires these data points for reading as well. @@ -328,66 +332,77 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, subtypes, respectively. """ - try: - self._mode = mode - mode_int = _open_modes[self._mode] - except KeyError: + if mode is None: + mode = getattr(file, 'mode', None) + if not isinstance(mode, str): + raise TypeError("Invalid mode: %s" % repr(mode)) + modes = set(mode) + if modes.difference('xrwb+') or len(mode) > len(modes): raise ValueError("Invalid mode: %s" % repr(mode)) + if len(modes.intersection('xrw')) != 1: + raise ValueError("mode must contain exactly one of 'xrw'") + self._mode = mode + + if '+' in mode: + mode_int = _snd.SFM_RDWR + elif 'r' in mode: + mode_int = _snd.SFM_READ + else: + mode_int = _snd.SFM_WRITE - original_format = format - filename = getattr(file, 'name', file) - file_extension = str(filename).rsplit('.', 1)[-1].upper() - if format is None and ('w' in self.mode or - file_extension == 'RAW'): - if file_extension not in _formats: - if self.mode == 'w': - raise TypeError( - "No format specified and unable to get format from " - "file extension: %s" % repr(filename)) - else: - format = file_extension + old_fmt = format + if format is None: + filename = getattr(file, 'name', file) + format = str(filename).rsplit('.', 1)[-1].upper() + if format not in _formats and 'r' not in mode: + raise TypeError( + "No format specified and unable to get format from " + "file extension: %s" % repr(filename)) self._info = _ffi.new("SF_INFO*") - if self.mode == 'w' or str(format).upper() == 'RAW': + if 'r' not in mode or str(format).upper() == 'RAW': if samplerate is None: raise TypeError("samplerate must be specified") self._info.samplerate = samplerate if channels is None: raise TypeError("channels must be specified") self._info.channels = channels - if str(format).upper() == 'RAW' and subtype is None: - raise TypeError("RAW files must specify a subtype") self._info.format = _format_int(format, subtype, endian) - elif self.mode == 'rw': - if samplerate is not None: - self._info.samplerate = samplerate - if channels is not None: - self._info.channels = channels - if format is not None: - self._info.format = _format_int(format, subtype, endian) else: - if [samplerate, channels, original_format, subtype, endian] != \ - [None] * 5: - raise TypeError("Only allowed if mode='w' or format='RAW': " - "samplerate, channels, " - "format, subtype, endian") + if any(arg is not None for arg in (samplerate, channels, old_fmt, + subtype, endian)): + raise TypeError( + "Not allowed for existing files (except 'RAW'): " + "samplerate, channels, format, subtype, endian") + + if not closefd and not isinstance(file, int): + raise ValueError("closefd=False only allowed for file descriptors") self._name = file + if isinstance(file, str): - file = _ffi.new('char[]', file.encode()) - self._file = _snd.sf_open(file, mode_int, self._info) - elif isinstance(file, int): + if 'b' not in mode: + mode += 'b' + self._filestream = _builtins.open(file, mode, buffering=0) + # Note: self._filestream must be kept alive to avoid closing by GC + file = self._filestream.fileno() + + if isinstance(file, int): self._file = _snd.sf_open_fd(file, mode_int, self._info, closefd) elif all(hasattr(file, a) for a in ('seek', 'read', 'write', 'tell')): self._file = _snd.sf_open_virtual( self._init_virtual_io(file), mode_int, self._info, _ffi.NULL) self._name = str(file) else: - raise RuntimeError("file must be a filename, a file descriptor or " - "a file-like object with the methods " - "'seek()', 'read()', 'write()' and 'tell()'") + raise TypeError("file must be a filename, a file descriptor or " + "a file-like object with the methods " + "'seek()', 'read()', 'write()' and 'tell()'") self._handle_error() + if modes.issuperset('r+'): + # Move write pointer to 0 (like in Python file objects) + self.seek(0) + name = property(lambda self: self._name) mode = property(lambda self: self._mode) frames = property(lambda self: self._info.frames) @@ -502,9 +517,6 @@ def __setattr__(self, name, value): # access text data in the sound file through properties if name in _str_types: self._check_if_closed() - if self.mode == 'r': - raise RuntimeError("Can not change %s of file in read mode" % - repr(name)) data = _ffi.new('char[]', value.encode()) err = _snd.sf_set_string(self._file, _str_types[name], data) self._handle_error_number(err) @@ -544,10 +556,10 @@ def __getitem__(self, frame): "SoundFile can only be accessed in one or two dimensions") frame, second_frame = frame start, stop = self._get_slice_bounds(frame) - curr = self.seek(0, SEEK_CUR, 'r') - self.seek(start, SEEK_SET, 'r') + curr = self.seek(0, SEEK_CUR) + self.seek(start, SEEK_SET) data = self.read(stop - start) - self.seek(curr, SEEK_SET, 'r') + self.seek(curr, SEEK_SET) if second_frame: return data[(slice(None), second_frame)] else: @@ -557,17 +569,15 @@ def __setitem__(self, frame, data): # access the file as if it where a one-dimensional Numpy # array. Data must be in the form (frames x channels). # Both open slice bounds and negative values are allowed. - if self.mode == 'r': - raise RuntimeError("Cannot write to file opened in read mode") start, stop = self._get_slice_bounds(frame) if stop - start != len(data): raise IndexError( "Could not fit data of length %i into slice of length %i" % (len(data), stop - start)) - curr = self.seek(0, SEEK_CUR, 'w') - self.seek(start, SEEK_SET, 'w') + curr = self.seek(0, SEEK_CUR) + self.seek(start, SEEK_SET) self.write(data) - self.seek(curr, SEEK_SET, 'w') + self.seek(curr, SEEK_SET) return data def flush(self): @@ -582,9 +592,13 @@ def close(self): self.flush() err = _snd.sf_close(self._file) self._file = None + try: + self._filestream.close() + except Exception: + pass self._handle_error_number(err) - def seek(self, frames, whence=SEEK_SET, which=None): + def seek(self, frames, whence=SEEK_SET): """Set the read and/or write position. By default (whence=SEEK_SET), frames are counted from the @@ -606,11 +620,6 @@ def seek(self, frames, whence=SEEK_SET, which=None): """ self._check_if_closed() - if which is not None: - if which != 'rw' and which in self.mode: - whence |= _open_modes[which] - else: - raise ValueError("Invalid which: %s" % repr(which)) return _snd.sf_seek(self._file, frames, whence) def _check_array(self, array): @@ -634,15 +643,19 @@ def _create_empty_array(self, frames, always_2d, dtype): def _read_or_write(self, funcname, array, frames): # Call into libsndfile + self._check_if_closed() + ffi_type = _ffi_types[array.dtype] assert array.flags.c_contiguous assert array.dtype.itemsize == _ffi.sizeof(ffi_type) assert array.size >= frames * self.channels + curr = self.seek(0, SEEK_CUR) func = getattr(_snd, funcname + ffi_type) ptr = _ffi.cast(ffi_type + '*', array.ctypes.data) frames = func(self._file, ptr, frames) self._handle_error() + self.seek(curr + frames, SEEK_SET) # Update read and write position return frames def read(self, frames=-1, dtype='float64', always_2d=True, @@ -671,12 +684,8 @@ def read(self, frames=-1, dtype='float64', always_2d=True, containing all valid frames is returned. """ - self._check_if_closed() - if self.mode == 'w': - raise RuntimeError("Cannot read from file opened in write mode") - if out is None: - remaining_frames = self.frames - self.seek(0, SEEK_CUR, 'r') + remaining_frames = self.frames - self.seek(0, SEEK_CUR) if frames < 0 or (frames > remaining_frames and fill_value is None): frames = remaining_frames @@ -709,10 +718,6 @@ def write(self, data): array or as one-dimensional array for mono signals. """ - self._check_if_closed() - if self.mode == 'r': - raise RuntimeError("Cannot write to file opened in read mode") - # no copy is made if data has already the correct memory layout: data = _np.ascontiguousarray(data) @@ -720,9 +725,9 @@ def write(self, data): written = self._read_or_write('sf_writef_', data, len(data)) assert written == len(data) - curr = self.seek(0, SEEK_CUR, 'w') - self._info.frames = self.seek(0, SEEK_END, 'w') - self.seek(curr, SEEK_SET, 'w') + curr = self.seek(0, SEEK_CUR) + self._info.frames = self.seek(0, SEEK_END) + self.seek(curr, SEEK_SET) def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', always_2d=True, fill_value=None, out=None): @@ -742,8 +747,8 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', than blocksize. """ - if self.mode == 'w': - raise RuntimeError("blocks() is not allowed in write mode") + if 'r' not in self.mode and '+' not in self.mode: + raise RuntimeError("blocks() is not allowed in write-only mode") if out is None: if blocksize is None: @@ -754,7 +759,7 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', "Only one of {blocksize, out} may be specified") blocksize = len(out) - remaining_frames = self.frames - self.seek(0, SEEK_CUR, 'r') + remaining_frames = self.frames - self.seek(0, SEEK_CUR) if frames < 0 or (fill_value is None and frames > remaining_frames): frames = remaining_frames @@ -766,7 +771,7 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64', block = self.read(blocksize, dtype, always_2d, fill_value, out) frames -= blocksize if frames > 0: - self.seek(-overlap, SEEK_CUR, 'r') + self.seek(-overlap, SEEK_CUR) frames += overlap yield block diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index 633738e..97b153b 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -3,6 +3,9 @@ import os import shutil import pytest +import sys + +PY2 = sys.version_info[0] == 2 data_stereo = np.array([[1.0, -1.0], [0.75, -0.75], @@ -66,12 +69,12 @@ def file_w(request): @pytest.fixture(params=open_variants) -def file_stereo_rw_existing(request): +def file_stereo_rplus(request): return _file_copy(request, filename_stereo, os.O_RDWR, 'r+b') @pytest.fixture(params=open_variants) -def file_rw_new(request): +def file_wplus(request): return _file_new(request, os.O_CREAT | os.O_RDWR, 'w+b') @@ -88,14 +91,14 @@ def sf_stereo_w(file_w): @pytest.yield_fixture -def sf_stereo_rw_existing(file_stereo_rw_existing): - with sf.open(file_stereo_rw_existing, 'rw') as f: +def sf_stereo_rplus(file_stereo_rplus): + with sf.open(file_stereo_rplus, 'r+') as f: yield f @pytest.yield_fixture -def sf_stereo_rw_new(file_rw_new): - with sf.open(file_rw_new, 'rw', 44100, 2, +def sf_stereo_wplus(file_wplus): + with sf.open(file_wplus, 'w+', 44100, 2, format='WAV', subtype='FLOAT') as f: yield f @@ -296,14 +299,14 @@ def test_blocks_mono(): assert_equal_list_of_arrays(blocks, [[0, 1, 2], [-2, -1, 0]]) -def test_blocks_rw_existing(sf_stereo_rw_existing): - blocks = list(sf_stereo_rw_existing.blocks(blocksize=2)) +def test_blocks_rplus(sf_stereo_rplus): + blocks = list(sf_stereo_rplus.blocks(blocksize=2)) assert_equal_list_of_arrays(blocks, [data_stereo[0:2], data_stereo[2:4]]) -def test_blocks_rw_new(sf_stereo_rw_new): - """There is nothing to yield in a new 'rw' file.""" - blocks = list(sf_stereo_rw_new.blocks(blocksize=2, frames=666)) +def test_blocks_wplus(sf_stereo_wplus): + """There is nothing to yield in a 'w+' file.""" + blocks = list(sf_stereo_wplus.blocks(blocksize=2, frames=666)) assert blocks == [] @@ -312,6 +315,100 @@ def test_blocks_write(sf_stereo_w): list(sf_stereo_w.blocks(blocksize=2)) +# ----------------------------------------------------------------------------- +# Test open() +# ----------------------------------------------------------------------------- + + +def test_open_with_invalid_file(): + with pytest.raises(TypeError) as excinfo: + sf.open(3.1415) + assert "filename" in str(excinfo.value) + + +def test_open_with_invalid_mode(): + with pytest.raises(TypeError) as excinfo: + sf.open(filename_stereo, 42) + assert "Invalid mode: 42" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + sf.open(filename_stereo, 'rr') + assert "Invalid mode: 'rr'" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + sf.open(filename_stereo, 'rw') + assert "exactly one of 'xrw'" in str(excinfo.value) + + +def test_open_with_more_invalid_arguments(): + with pytest.raises(TypeError) as excinfo: + sf.open(filename_new, 'w', samplerate=3.1415, channels=2) + assert "integer" in str(excinfo.value) + with pytest.raises(TypeError) as excinfo: + sf.open(filename_new, 'w', samplerate=44100, channels=3.1415) + assert "integer" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + sf.open(filename_new, 'w', 44100, 2, format='WAF') + assert "Invalid format string" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + sf.open(filename_new, 'w', 44100, 2, subtype='PCM16') + assert "Invalid subtype string" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + sf.open(filename_new, 'w', 44100, 2, endian='BOTH') + assert "Invalid endian-ness" in str(excinfo.value) + with pytest.raises(ValueError) as excinfo: + sf.open(filename_stereo, closefd=False) + assert "closefd=False" in str(excinfo.value) + + +def test_open_r_and_rplus_with_too_many_arguments(): + for mode in 'r', 'r+': + with pytest.raises(TypeError) as excinfo: + sf.open(filename_stereo, mode, samplerate=44100) + assert "Not allowed" in str(excinfo.value) + with pytest.raises(TypeError) as excinfo: + sf.open(filename_stereo, mode, channels=2) + assert "Not allowed" in str(excinfo.value) + with pytest.raises(TypeError) as excinfo: + sf.open(filename_stereo, mode, format='WAV') + assert "Not allowed" in str(excinfo.value) + with pytest.raises(TypeError) as excinfo: + sf.open(filename_stereo, mode, subtype='FLOAT') + assert "Not allowed" in str(excinfo.value) + with pytest.raises(TypeError) as excinfo: + sf.open(filename_stereo, mode, endian='FILE') + assert "Not allowed" in str(excinfo.value) + + +def test_open_w_and_wplus_with_too_few_arguments(): + filename = 'not_existing.xyz' + for mode in 'w', 'w+': + with pytest.raises(TypeError) as excinfo: + sf.open(filename, mode, samplerate=44100, channels=2) + assert "No format specified" in str(excinfo.value) + with pytest.raises(TypeError) as excinfo: + sf.open(filename, mode, samplerate=44100, format='WAV') + assert "channels" in str(excinfo.value) + with pytest.raises(TypeError) as excinfo: + sf.open(filename, mode, channels=2, format='WAV') + assert "samplerate" in str(excinfo.value) + + +def test_open_with_mode_is_none(): + with pytest.raises(TypeError) as excinfo: + sf.open(filename_stereo, mode=None) + assert "Invalid mode: None" in str(excinfo.value) + with open(filename_stereo, 'rb') as fobj: + with sf.open(fobj, mode=None) as f: + assert f.mode == 'rb' + + +@pytest.mark.skipif(PY2, reason="mode='x' not supported in Python 2") +def test_open_with_mode_is_x(): + with pytest.raises(FileExistsError): + sf.open(filename_stereo, 'x', 44100, 2) + with pytest.raises(FileExistsError): + sf.open(filename_stereo, 'x+', 44100, 2) + + # ----------------------------------------------------------------------------- # Test file metadata # ----------------------------------------------------------------------------- @@ -338,8 +435,8 @@ def test_mode_should_be_in_write_mode(sf_stereo_w): assert len(sf_stereo_w) == 0 -def test_mode_should_be_in_readwrite_mode(sf_stereo_rw_existing): - assert sf_stereo_rw_existing.mode == 'rw' +def test_mode_should_be_in_readwrite_mode(sf_stereo_rplus): + assert sf_stereo_rplus.mode == 'r+' # ----------------------------------------------------------------------------- @@ -352,37 +449,19 @@ def test_seek_in_read_mode(sf_stereo_r): assert sf_stereo_r.seek(2) == 2 assert sf_stereo_r.seek(2, sf.SEEK_CUR) == 4 assert sf_stereo_r.seek(-2, sf.SEEK_END) == len(data_stereo) - 2 - assert sf_stereo_r.seek(2, which='r') == 2 assert sf_stereo_r.seek(666) == -1 assert sf_stereo_r.seek(-666) == -1 - with pytest.raises(ValueError): - sf_stereo_r.seek(2, which='w') def test_seek_in_write_mode(sf_stereo_w): assert sf_stereo_w.seek(0, sf.SEEK_CUR) == 0 - assert sf_stereo_w.seek(2, which='w') == 2 - with pytest.raises(ValueError): - sf_stereo_w.seek(2, which='r') - - -def test_initial_read_and_write_position(sf_stereo_rw_existing): - assert sf_stereo_rw_existing.seek(0, sf.SEEK_CUR, 'w') == len(data_stereo) - assert sf_stereo_rw_existing.seek(0, sf.SEEK_CUR, 'r') == 0 - # 'w' wins ... - assert sf_stereo_rw_existing.seek(0, sf.SEEK_CUR) == len(data_stereo) - # ... and moves read position: - assert sf_stereo_rw_existing.seek(0, sf.SEEK_CUR, 'r') == len(data_stereo) - - -def test_if_seek_write_advances_read_position(sf_stereo_rw_existing): - assert sf_stereo_rw_existing.seek(2, which='w') == 2 - assert sf_stereo_rw_existing.seek(0, sf.SEEK_CUR, 'r') == 0 + assert sf_stereo_w.seek(2) == 2 -def test_if_seek_read_advances_write_pointer(sf_stereo_rw_existing): - assert sf_stereo_rw_existing.seek(2, which='r') == 2 - assert sf_stereo_rw_existing.seek(0, sf.SEEK_CUR, 'w') == len(data_stereo) +def test_seek_in_rplus_mode(sf_stereo_rplus): + assert sf_stereo_rplus.seek(0, sf.SEEK_CUR) == 0 + assert sf_stereo_rplus.seek(2) == 2 + assert sf_stereo_rplus.seek(0, sf.SEEK_CUR) == 2 # ----------------------------------------------------------------------------- @@ -474,34 +553,33 @@ def test_write_flush_should_write_to_disk(sf_stereo_w): assert os.path.getsize(filename_new) == size + data_stereo.size * 2 -def test_rw_read_written_data(sf_stereo_rw_new): - sf_stereo_rw_new.seek(0) - sf_stereo_rw_new.write(data_stereo) - assert sf_stereo_rw_new.seek(0, sf.SEEK_CUR, 'w') == len(data_stereo) - assert sf_stereo_rw_new.seek(0, sf.SEEK_CUR, 'r') == 0 - assert np.all(sf_stereo_rw_new.read() == data_stereo) - assert sf_stereo_rw_new.seek(0, sf.SEEK_CUR, 'w') == len(data_stereo) - assert sf_stereo_rw_new.seek(0, sf.SEEK_CUR, 'r') == len(data_stereo) - sf_stereo_rw_new.close() +def test_wplus_read_written_data(sf_stereo_wplus): + sf_stereo_wplus.write(data_stereo) + assert sf_stereo_wplus.seek(0, sf.SEEK_CUR) == len(data_stereo) + sf_stereo_wplus.seek(0) + assert np.all(sf_stereo_wplus.read() == data_stereo) + assert sf_stereo_wplus.seek(0, sf.SEEK_CUR) == len(data_stereo) + sf_stereo_wplus.close() data, fs = sf.read(filename_new) assert np.all(data == data_stereo) -def test_rw_writing_using_indexing_should_write_but_not_advance_write_pointer( - sf_stereo_rw_new): +def test_wplus_writing_using_indexing_should_write_but_not_advance_write_pointer( + sf_stereo_wplus): data = np.ones((5, 2)) # grow file to make room for indexing - sf_stereo_rw_new.write(np.zeros((5, 2))) - position = sf_stereo_rw_new.seek(0, sf.SEEK_CUR, which='w') - sf_stereo_rw_new[:len(data)] = data - written_data = sf_stereo_rw_new[:len(data)] + sf_stereo_wplus.write(np.zeros((5, 2))) + position = sf_stereo_wplus.seek(0, sf.SEEK_CUR) + sf_stereo_wplus[:len(data)] = data + written_data = sf_stereo_wplus[:len(data)] assert np.all(data == written_data) - assert position == sf_stereo_rw_new.seek(0, sf.SEEK_CUR, which='w') + assert position == sf_stereo_wplus.seek(0, sf.SEEK_CUR) -def test_rw_append_data(sf_stereo_rw_existing): - sf_stereo_rw_existing.write(data_stereo / 2) - sf_stereo_rw_existing.close() +def test_rplus_append_data(sf_stereo_rplus): + sf_stereo_rplus.seek(0, sf.SEEK_END) + sf_stereo_rplus.write(data_stereo / 2) + sf_stereo_rplus.close() data, fs = sf.read(tempfilename) assert np.all(data[:len(data_stereo)] == data_stereo) assert np.all(data[len(data_stereo):] == data_stereo / 2) @@ -548,8 +626,7 @@ def test_non_file_attributes_should_not_save_to_disk(): def test_read_raw_files_should_read_data(): - with sf.open(filename_raw, samplerate=44100, - channels=1, subtype='PCM_16') as f: + with sf.open(filename_raw, 'r', 44100, 1, 'PCM_16') as f: assert np.all(f.read(dtype='int16') == data_mono) From 019cff915fdd4ce52c465cf5510cc771b9045d06 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 7 Aug 2014 11:18:06 +0200 Subject: [PATCH 2/7] Avoid catch-all exception handling --- pysoundfile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index d8c3211..e229b3b 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -386,6 +386,7 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, self._filestream = _builtins.open(file, mode, buffering=0) # Note: self._filestream must be kept alive to avoid closing by GC file = self._filestream.fileno() + closefd = False if isinstance(file, int): self._file = _snd.sf_open_fd(file, mode_int, self._info, closefd) @@ -426,6 +427,7 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, # avoid confusion if something goes wrong before assigning self._file: _file = None + _filestream = None def _init_virtual_io(self, file): @_ffi.callback("sf_vio_get_filelen") @@ -592,10 +594,8 @@ def close(self): self.flush() err = _snd.sf_close(self._file) self._file = None - try: + if self._filestream: self._filestream.close() - except Exception: - pass self._handle_error_number(err) def seek(self, frames, whence=SEEK_SET): From c971db2e83fd6e5fc5cf90daec199bd4f2109b3a Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Wed, 13 Aug 2014 18:39:01 +0200 Subject: [PATCH 3/7] Use sf_open_virtual() instead of sf_open_fd() if file name is given --- pysoundfile.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index e229b3b..c4d8812 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -383,10 +383,7 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, if isinstance(file, str): if 'b' not in mode: mode += 'b' - self._filestream = _builtins.open(file, mode, buffering=0) - # Note: self._filestream must be kept alive to avoid closing by GC - file = self._filestream.fileno() - closefd = False + file = self._filestream = _builtins.open(file, mode, buffering=0) if isinstance(file, int): self._file = _snd.sf_open_fd(file, mode_int, self._info, closefd) From 6e6783074202f76aed4c0f4a8597aadba5a1f339 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 14 Aug 2014 10:15:54 +0200 Subject: [PATCH 4/7] Shorter exception message on invalid file argument Information about what is allowed in the "file" argument should be written into the docstring, not into the error message. --- pysoundfile.py | 4 +--- tests/test_pysoundfile.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index c4d8812..47d8cb8 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -392,9 +392,7 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, self._init_virtual_io(file), mode_int, self._info, _ffi.NULL) self._name = str(file) else: - raise TypeError("file must be a filename, a file descriptor or " - "a file-like object with the methods " - "'seek()', 'read()', 'write()' and 'tell()'") + raise TypeError("Invalid file: %s" % repr(file)) self._handle_error() if modes.issuperset('r+'): diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index 97b153b..fac3943 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -323,7 +323,7 @@ def test_blocks_write(sf_stereo_w): def test_open_with_invalid_file(): with pytest.raises(TypeError) as excinfo: sf.open(3.1415) - assert "filename" in str(excinfo.value) + assert "Invalid file" in str(excinfo.value) def test_open_with_invalid_mode(): From c6b827586e750ecbc8512f1d405314acf25c07b4 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 14 Aug 2014 14:55:43 +0200 Subject: [PATCH 5/7] Fix name property --- pysoundfile.py | 9 +++------ tests/test_pysoundfile.py | 8 +++++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 47d8cb8..30241f0 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -351,13 +351,13 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, mode_int = _snd.SFM_WRITE old_fmt = format + self._name = getattr(file, 'name', file) if format is None: - filename = getattr(file, 'name', file) - format = str(filename).rsplit('.', 1)[-1].upper() + format = str(self.name).rsplit('.', 1)[-1].upper() if format not in _formats and 'r' not in mode: raise TypeError( "No format specified and unable to get format from " - "file extension: %s" % repr(filename)) + "file extension: %s" % repr(self.name)) self._info = _ffi.new("SF_INFO*") if 'r' not in mode or str(format).upper() == 'RAW': @@ -378,8 +378,6 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, if not closefd and not isinstance(file, int): raise ValueError("closefd=False only allowed for file descriptors") - self._name = file - if isinstance(file, str): if 'b' not in mode: mode += 'b' @@ -390,7 +388,6 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, elif all(hasattr(file, a) for a in ('seek', 'read', 'write', 'tell')): self._file = _snd.sf_open_virtual( self._init_virtual_io(file), mode_int, self._info, _ffi.NULL) - self._name = str(file) else: raise TypeError("Invalid file: %s" % repr(file)) self._handle_error() diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index fac3943..c87aa9c 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -419,14 +419,20 @@ def test_file_content(sf_stereo_r): def test_file_attributes_in_read_mode(sf_stereo_r): + if not isinstance(sf_stereo_r.name, int): + assert sf_stereo_r.name == filename_stereo assert sf_stereo_r.mode == 'r' - assert sf_stereo_r.channels == 2 + assert sf_stereo_r.frames == len(data_stereo) assert sf_stereo_r.samplerate == 44100 + assert sf_stereo_r.channels == 2 assert sf_stereo_r.format == 'WAV' assert sf_stereo_r.subtype == 'FLOAT' assert sf_stereo_r.endian == 'FILE' assert sf_stereo_r.format_info == 'WAV (Microsoft)' assert sf_stereo_r.subtype_info == '32 bit float' + assert sf_stereo_r.sections == 1 + assert sf_stereo_r.seekable is True + assert sf_stereo_r.closed is False assert len(sf_stereo_r) == len(data_stereo) From 8e3f6ee27cf3e15ddc86c19f2abf916db8eb9216 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Thu, 14 Aug 2014 15:01:15 +0200 Subject: [PATCH 6/7] Change seekable from property to method ... in order to be compatible with Python file objects: https://docs.python.org/3.4/library/io.html#io.IOBase.seekable --- pysoundfile.py | 5 ++++- tests/test_pysoundfile.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 30241f0..4e89d49 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -414,13 +414,16 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, lambda self: _format_info(self._info.format & _snd.SF_FORMAT_SUBMASK)[1]) sections = property(lambda self: self._info.sections) - seekable = property(lambda self: self._info.seekable == _snd.SF_TRUE) closed = property(lambda self: self._file is None) # avoid confusion if something goes wrong before assigning self._file: _file = None _filestream = None + def seekable(self): + """Return True if the file supports seeking.""" + return self._info.seekable == _snd.SF_TRUE + def _init_virtual_io(self, file): @_ffi.callback("sf_vio_get_filelen") def vio_get_filelen(user_data): diff --git a/tests/test_pysoundfile.py b/tests/test_pysoundfile.py index c87aa9c..775ec15 100644 --- a/tests/test_pysoundfile.py +++ b/tests/test_pysoundfile.py @@ -431,8 +431,8 @@ def test_file_attributes_in_read_mode(sf_stereo_r): assert sf_stereo_r.format_info == 'WAV (Microsoft)' assert sf_stereo_r.subtype_info == '32 bit float' assert sf_stereo_r.sections == 1 - assert sf_stereo_r.seekable is True assert sf_stereo_r.closed is False + assert sf_stereo_r.seekable() is True assert len(sf_stereo_r) == len(data_stereo) From 046023c793238cbc17cec304118172637996f0b6 Mon Sep 17 00:00:00 2001 From: Matthias Geier Date: Mon, 18 Aug 2014 17:19:10 +0200 Subject: [PATCH 7/7] Check if seekable() before calling seek() --- pysoundfile.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pysoundfile.py b/pysoundfile.py index 4e89d49..028d5da 100644 --- a/pysoundfile.py +++ b/pysoundfile.py @@ -392,7 +392,7 @@ def __init__(self, file, mode='r', samplerate=None, channels=None, raise TypeError("Invalid file: %s" % repr(file)) self._handle_error() - if modes.issuperset('r+'): + if modes.issuperset('r+') and self.seekable(): # Move write pointer to 0 (like in Python file objects) self.seek(0) @@ -645,12 +645,14 @@ def _read_or_write(self, funcname, array, frames): assert array.dtype.itemsize == _ffi.sizeof(ffi_type) assert array.size >= frames * self.channels - curr = self.seek(0, SEEK_CUR) + if self.seekable(): + curr = self.seek(0, SEEK_CUR) func = getattr(_snd, funcname + ffi_type) ptr = _ffi.cast(ffi_type + '*', array.ctypes.data) frames = func(self._file, ptr, frames) self._handle_error() - self.seek(curr + frames, SEEK_SET) # Update read and write position + if self.seekable(): + self.seek(curr + frames, SEEK_SET) # Update read & write position return frames def read(self, frames=-1, dtype='float64', always_2d=True,