Skip to content

Commit a7f7ef1

Browse files
authored
Merge pull request #272 from Jajcus/SoundFileError_exception
SoundFileError exception added
2 parents 4f43e6f + 36be05a commit a7f7ef1

File tree

3 files changed

+96
-16
lines changed

3 files changed

+96
-16
lines changed

README.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ Binaries for Python Extension Packages <http://www.lfd.uci.edu/~gohlke/pythonlib
7171

7272
.. _Anaconda: https://www.continuum.io/downloads
7373

74+
Error Reporting
75+
---------------
76+
77+
In case of API usage errors the ``soundfile`` module raises the usual `ValueError` or `TypeError`.
78+
79+
For other errors `SoundFileError` is raised (used to be `RuntimeError`).
80+
Particularly, a `LibsndfileError` subclass of this exception is raised on
81+
errors reported by the libsndfile library. In that case the exception object
82+
provides the libsndfile internal error code in the `LibsndfileError.code` attribute and the raw
83+
libsndfile error message in the `LibsndfileError.error_string` attribute.
84+
7485
Read/Write Functions
7586
--------------------
7687

soundfile.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,7 +1072,7 @@ def blocks(self, blocksize=None, overlap=0, frames=-1, dtype='float64',
10721072
import numpy as np
10731073

10741074
if 'r' not in self.mode and '+' not in self.mode:
1075-
raise RuntimeError("blocks() is not allowed in write-only mode")
1075+
raise SoundFileRuntimeError("blocks() is not allowed in write-only mode")
10761076

10771077
if out is None:
10781078
if blocksize is None:
@@ -1130,7 +1130,9 @@ def truncate(self, frames=None):
11301130
_ffi.new("sf_count_t*", frames),
11311131
_ffi.sizeof("sf_count_t"))
11321132
if err:
1133-
raise RuntimeError("Error truncating the file")
1133+
# get the actual error code
1134+
err = _snd.sf_error(self._file)
1135+
raise LibsndfileError(err, "Error truncating the file")
11341136
self._info.frames = frames
11351137

11361138
def flush(self):
@@ -1256,7 +1258,7 @@ def _check_if_closed(self):
12561258
12571259
"""
12581260
if self.closed:
1259-
raise RuntimeError("I/O operation on closed file")
1261+
raise SoundFileRuntimeError("I/O operation on closed file")
12601262

12611263
def _check_frames(self, frames, fill_value):
12621264
"""Reduce frames to no more than are available in the file."""
@@ -1350,10 +1352,9 @@ def _prepare_read(self, start, stop, frames):
13501352

13511353

13521354
def _error_check(err, prefix=""):
1353-
"""Pretty-print a numerical error code if there is an error."""
1355+
"""Raise LibsndfileError if there is an error."""
13541356
if err != 0:
1355-
err_str = _snd.sf_error_number(err)
1356-
raise RuntimeError(prefix + _ffi.string(err_str).decode('utf-8', 'replace'))
1357+
raise LibsndfileError(err, prefix=prefix)
13571358

13581359

13591360
def _format_int(format, subtype, endian):
@@ -1508,3 +1509,37 @@ def _has_virtual_io_attrs(file, mode_int):
15081509
hasattr(file, 'write') or readonly,
15091510
hasattr(file, 'read') or hasattr(file, 'readinto') or writeonly,
15101511
])
1512+
1513+
1514+
class SoundFileError(Exception):
1515+
"""Base class for all SoundFile-specific errors."""
1516+
pass
1517+
1518+
class SoundFileRuntimeError(SoundFileError, RuntimeError):
1519+
"""SoundFile runtime error.
1520+
1521+
Errors that used to be `RuntimeError`."""
1522+
pass
1523+
1524+
class LibsndfileError(SoundFileRuntimeError):
1525+
"""libsndfile errors.
1526+
1527+
1528+
Attributes
1529+
----------
1530+
code
1531+
libsndfile internal error number.
1532+
"""
1533+
def __init__(self, code, prefix=""):
1534+
SoundFileRuntimeError.__init__(self, code, prefix)
1535+
self.code = code
1536+
self.prefix = prefix
1537+
1538+
@property
1539+
def error_string(self):
1540+
"""Raw libsndfile error message."""
1541+
err_str = _snd.sf_error_number(self.code)
1542+
return _ffi.string(err_str).decode('utf-8', 'replace')
1543+
1544+
def __str__(self):
1545+
return self.prefix + self.error_string

tests/test_pysoundfile.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,12 @@ def test_read_mono_into_2d_out(file_mono_r):
232232

233233

234234
def test_read_non_existing_file():
235-
with pytest.raises(RuntimeError) as excinfo:
235+
with pytest.raises(sf.SoundFileError) as excinfo:
236236
sf.read("i_do_not_exist.wav")
237237
assert "Error opening 'i_do_not_exist.wav'" in str(excinfo.value)
238238

239239

240+
240241
# -----------------------------------------------------------------------------
241242
# Test write() function
242243
# -----------------------------------------------------------------------------
@@ -418,7 +419,7 @@ def test_blocks_wplus(sf_stereo_wplus):
418419

419420

420421
def test_blocks_write(sf_stereo_w):
421-
with pytest.raises(RuntimeError):
422+
with pytest.raises(sf.SoundFileError):
422423
list(sf_stereo_w.blocks(blocksize=2))
423424

424425

@@ -644,9 +645,9 @@ def test_seek_in_read_mode(sf_stereo_r):
644645
assert sf_stereo_r.tell() == 2
645646
assert sf_stereo_r.seek(2, sf.SEEK_CUR) == 4
646647
assert sf_stereo_r.seek(-2, sf.SEEK_END) == len(data_stereo) - 2
647-
with pytest.raises(RuntimeError):
648+
with pytest.raises(sf.SoundFileError):
648649
sf_stereo_r.seek(666)
649-
with pytest.raises(RuntimeError):
650+
with pytest.raises(sf.SoundFileError):
650651
sf_stereo_r.seek(-666)
651652

652653

@@ -685,8 +686,9 @@ def test_truncate(file_stereo_rplus, use_default):
685686
else:
686687
# file objects don't support truncate()
687688
with sf.SoundFile(file_stereo_rplus, 'r+', closefd=False) as f:
688-
with pytest.raises(RuntimeError) as excinfo:
689+
with pytest.raises(sf.SoundFileError) as excinfo:
689690
f.truncate()
691+
assert isinstance(excinfo.value, RuntimeError)
690692
assert "Error truncating" in str(excinfo.value)
691693

692694

@@ -696,7 +698,7 @@ def test_truncate(file_stereo_rplus, use_default):
696698

697699

698700
def test_read_write_only(sf_stereo_w):
699-
with pytest.raises(RuntimeError):
701+
with pytest.raises(sf.SoundFileError):
700702
sf_stereo_w.read(2)
701703

702704

@@ -796,7 +798,7 @@ def test_buffer_read_into(sf_stereo_r):
796798

797799

798800
def test_write_to_read_only_file_should_fail(sf_stereo_r):
799-
with pytest.raises(RuntimeError):
801+
with pytest.raises(sf.SoundFileError):
800802
sf_stereo_r.write(data_stereo)
801803

802804

@@ -887,8 +889,9 @@ def test_closing_should_close_file(file_stereo_r):
887889
def test_anything_on_closed_file(file_stereo_r):
888890
with sf.SoundFile(file_stereo_r) as f:
889891
pass
890-
with pytest.raises(RuntimeError) as excinfo:
892+
with pytest.raises(sf.SoundFileError) as excinfo:
891893
f.seek(0)
894+
assert isinstance(excinfo.value, RuntimeError)
892895
assert "closed" in str(excinfo.value)
893896

894897

@@ -1014,8 +1017,9 @@ def test_write_non_seekable_file(file_w):
10141017
f.write(data_mono)
10151018
assert f.frames == len(data_mono)
10161019

1017-
with pytest.raises(RuntimeError) as excinfo:
1020+
with pytest.raises(sf.SoundFileError) as excinfo:
10181021
f.seek(2)
1022+
assert isinstance(excinfo.value, RuntimeError)
10191023
assert "unseekable" in str(excinfo.value)
10201024

10211025
with sf.SoundFile(filename_new) as f:
@@ -1026,8 +1030,9 @@ def test_write_non_seekable_file(file_w):
10261030
data = f.read(666, dtype='int16')
10271031
assert np.all(data == data_mono[3:])
10281032

1029-
with pytest.raises(RuntimeError) as excinfo:
1033+
with pytest.raises(sf.SoundFileError) as excinfo:
10301034
f.seek(2)
1035+
assert isinstance(excinfo.value, RuntimeError)
10311036
assert "unseekable" in str(excinfo.value)
10321037

10331038
with pytest.raises(ValueError) as excinfo:
@@ -1041,3 +1046,32 @@ def test_write_non_seekable_file(file_w):
10411046
with pytest.raises(ValueError) as excinfo:
10421047
sf.read(filename_new, start=3)
10431048
assert "start is only allowed for seekable files" in str(excinfo.value)
1049+
1050+
1051+
# -----------------------------------------------------------------------------
1052+
# Test LibsndfileError
1053+
# -----------------------------------------------------------------------------
1054+
1055+
def test_libsndfile_error():
1056+
err = sf.LibsndfileError(2)
1057+
assert isinstance(err, sf.SoundFileError)
1058+
assert err.error_string # cannot assume exact message generated by libsndfile
1059+
assert str(err) == err.error_string
1060+
1061+
def test_libsndfile_error_with_prefix():
1062+
err = sf.LibsndfileError(2, prefix="XX ")
1063+
assert isinstance(err, sf.SoundFileError)
1064+
assert err.error_string # cannot assume exact message generated by libsndfile
1065+
assert str(err) == "XX " + err.error_string
1066+
1067+
def test_rasing_libsndfile_error():
1068+
with pytest.raises(sf.LibsndfileError) as excinfo:
1069+
sf.read("i_do_not_exist.wav")
1070+
assert isinstance(excinfo.value, sf.SoundFileError)
1071+
assert isinstance(excinfo.value, RuntimeError)
1072+
assert excinfo.value.code == 2 # SF_ERR_SYSTEM
1073+
if sys.version_info[0] == 2:
1074+
assert isinstance(excinfo.value.error_string, unicode)
1075+
else:
1076+
assert isinstance(excinfo.value.error_string, str)
1077+
assert excinfo.value.error_string in str(excinfo.value)

0 commit comments

Comments
 (0)