Repository: pyfar/pyfar
Pull request #674 (2024-08-23 13:37:40)

Disable getting specific samples or frequencies from a signal through slicing
- [x] As discussed during the weekly, I forced the `__getitem__` methods of the audio classes to throw an error.
- [x] Update tests accordingly
- [x] Opened issue in gallery to make sure we update the notebooks once pyfar 0.6.9 is released:  https://github.com/pyfar/gallery/issues/79



Patch:
diff --git a/pyfar/classes/audio.py b/pyfar/classes/audio.py
index e256bca2..0d69efde 100644
--- a/pyfar/classes/audio.py
+++ b/pyfar/classes/audio.py
@@ -166,7 +166,23 @@ class _Audio():
 
 
         """
-        data = self._data[key]
+
+        # add empty slice at the end to always get all data contained in last
+        # dimension (samples or frequency bins)
+        if hasattr(key, '__iter__'):
+            key = (*key, slice(None))
+
+        # try indexing and raise verbose errors if it fails
+        try:
+            data = self._data[key]
+        except IndexError as Error:
+            if 'too many indices for array' in str(Error):
+                raise IndexError((
+                    f'Indexed dimensions must not exceed the channel '
+                    f'dimension (cdim), which is {len(self.cshape)}'))
+            else:
+                raise Error
+
         return self._return_item(data)
 
     def __setitem__(self, key, value):


Test patch:
diff --git a/tests/test_audio_frequency_data.py b/tests/test_audio_frequency_data.py
index 59188369..823de31f 100644
--- a/tests/test_audio_frequency_data.py
+++ b/tests/test_audio_frequency_data.py
@@ -150,6 +150,20 @@ def test_magic_getitem_slice():
     npt.assert_allclose(FrequencyData(data[0], freqs)._data, freq[0]._data)
 
 
+def test_magic_getitem_error():
+    """
+    Test if indexing that would return a subset of the frequency bins raises a
+    key error.
+    """
+    freq = pf.FrequencyData([[0, 0, 0], [1, 1, 1]], [0, 1, 3])
+    # manually indexing too many dimensions
+    with pytest.raises(IndexError, match='Indexed dimensions must not exceed'):
+        freq[0, 1]
+    # indexing too many dimensions with ellipsis operator
+    with pytest.raises(IndexError, match='Indexed dimensions must not exceed'):
+        freq[0, 0, ..., 1]
+
+
 def test_magic_setitem():
     """Test the setitem for FrequencyData."""
     freqs = [0, .1, .3]
diff --git a/tests/test_audio_signal.py b/tests/test_audio_signal.py
index e7f36a76..1321ffc9 100644
--- a/tests/test_audio_signal.py
+++ b/tests/test_audio_signal.py
@@ -327,6 +327,29 @@ def test_magic_getitem_allslice():
     npt.assert_allclose(signal[:]._data, time[:])
 
 
+def test_magic_getitem_ellipsis():
+    """Test slicing operations by the magic function __getitem__."""
+    signal = pf.Signal([[[1, 1, 1], [2, 2, 2]]], 44100)
+    npt.assert_allclose(signal[..., 0].time, np.atleast_2d([1, 1, 1]))
+    assert signal[..., 0].time.shape == (1, 3)
+
+
+@pytest.mark.parametrize('domain', ['time', 'freq'])
+def test_magic_getitem_error(domain):
+    """
+    Test if indexing that would return a subset of the samples or frequency
+    bins raises a key error.
+    """
+    signal = pf.Signal([[0, 0, 0, 0, 0], [1, 1, 1, 1, 1]], 1)
+    signal.domain = domain
+    # manually indexing too many dimensions
+    with pytest.raises(IndexError, match='Indexed dimensions must not exceed'):
+        signal[0, 1]
+    # indexing too many dimensions with ellipsis operator
+    with pytest.raises(IndexError, match='Indexed dimensions must not exceed'):
+        signal[0, 0, ..., 1]
+
+
 def test_magic_setitem():
     """Test the magic function __setitem__."""
     signal = Signal([1, 2, 3], 44100)
diff --git a/tests/test_audio_time_data.py b/tests/test_audio_time_data.py
index 03a83bf4..bf6bec86 100644
--- a/tests/test_audio_time_data.py
+++ b/tests/test_audio_time_data.py
@@ -145,6 +145,20 @@ def test_magic_setitem():
     npt.assert_allclose(time_a.time, np.asarray([[2, 0, -2], [1, 0, -1]]))
 
 
+def test_magic_getitem_error():
+    """
+    Test if indexing that would return a subset of the frequency bins raises a
+    key error.
+    """
+    time = pf.TimeData([[0, 0, 0], [1, 1, 1]], [0, 1, 3])
+    # manually indexing too many dimensions
+    with pytest.raises(IndexError, match='Indexed dimensions must not exceed'):
+        time[0, 1]
+    # indexing too many dimensions with ellipsis operator
+    with pytest.raises(IndexError, match='Indexed dimensions must not exceed'):
+        time[0, 0, ..., 1]
+
+
 def test_magic_setitem_wrong_n_samples():
     """Test the setimtem for TimeData with wrong number of samples."""
 
diff --git a/tests/test_filter_classes.py b/tests/test_filter_classes.py
index 7a2599fc..032bbf78 100644
--- a/tests/test_filter_classes.py
+++ b/tests/test_filter_classes.py
@@ -352,11 +352,11 @@ def test_blockwise_processing(Filter):
 
     # filter in two blocks with correct handling of the state
     Filter.init_state(signal.cshape, 'zeros')
-    block_a = Filter.process(signal[0, :3], reset=False)
-    block_b = Filter.process(signal[0, 3:], reset=False)
+    block_a = Filter.process(pf.Signal(signal.time[0, :3], 44100), reset=False)
+    block_b = Filter.process(pf.Signal(signal.time[0, 3:], 44100), reset=False)
     # outputs have to be identical in this case
-    npt.assert_array_equal(complete[0, :3].time, block_a.time)
-    npt.assert_array_equal(complete[0, 3:].time, block_b.time)
+    npt.assert_array_equal(np.atleast_2d(complete.time[0, :3]), block_a.time)
+    npt.assert_array_equal(np.atleast_2d(complete.time[0, 3:]), block_b.time)
 
 
 def test_blockwise_processing_with_coefficients_exchange():
@@ -371,11 +371,11 @@ def test_blockwise_processing_with_coefficients_exchange():
     filter = pf.FilterFIR([coefficients_1], 44100)
     filter.init_state(input.cshape, state='zeros')
     # process first block
-    filter_1 = filter.process(input[..., :2])
+    filter_1 = filter.process(pf.Signal(input.time[..., :2], 44100))
     # update filter coefficients
     filter.coefficients = [coefficients_2]
     # process second block
-    filter_2 = filter.process(input[..., 2:])
+    filter_2 = filter.process(pf.Signal(input.time[..., 2:], 44100))
 
     # overlap and add time variant filtering
     # first block