Repository: CPJKU/partitura

Issue:
Error checking for equality in `PerformedNote`
There is a minor bug in the `__eq__` method in `PerformedNote`. Here is a small example:

```python
import partitura as pt
from partitura import EXAMPLE_MIDI

perf = pt.load_performance_midi(EXAMPLE_MIDI)

notes = perf[0].notes

test_note = notes[0]
for note in notes:

    crit = note == test_note

    if crit:
        print("Equal notes")
```


Which results in the following traceback

```bash
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 11
      8 test_note = notes[0]
      9 for note in notes:
---> 11     crit = note == test_note
     13     if crit:
     14         print("Equal notes")

File ~/Repos/partitura/partitura/performance.py:365, in PerformedNote.__eq__(self, other)
    364 def __eq__(self, other):
--> 365     if not isinstance(PerformedNote):
    366         return False
    367     if not self.keys() == other.keys():

TypeError: isinstance expected 2 arguments, got 1
```

Line 365 in `partitura/partitura/performance.py` is incorrect, and the fix should be fairly simple.

Discussion:

Solution patch:
diff --git a/partitura/performance.py b/partitura/performance.py
index a04e726..95d91ed 100644
--- a/partitura/performance.py
+++ b/partitura/performance.py
@@ -362,7 +362,7 @@ class PerformedNote:
         return f"PerformedNote: {self['id']}"
 
     def __eq__(self, other):
-        if not isinstance(PerformedNote):
+        if not isinstance(other, PerformedNote):
             return False
         if not self.keys() == other.keys():
             return False


Test patch:
diff --git a/tests/test_performance.py b/tests/test_performance.py
index 2c86434..ab40ae2 100644
--- a/tests/test_performance.py
+++ b/tests/test_performance.py
@@ -6,7 +6,7 @@ This module contains test functions for the `performance` module
 import unittest
 import numpy as np
 
-from partitura.performance import PerformedPart, Performance
+from partitura.performance import PerformedPart, Performance, PerformedNote
 
 RNG = np.random.RandomState(1984)
 
@@ -138,3 +138,195 @@ def generate_random_note_array(n_notes=100, beat_period=0.5, n_tracks=3):
             idx = track_idxs[i * track_length :]
         note_array["track"][idx] = i
     return note_array
+
+
+class TestPerformedNote(unittest.TestCase):
+    def test_initialization(self):
+        """
+        Test that the notes are initialized correctly
+        """
+        pnote = PerformedNote(
+            {
+                "pitch": 60,
+                "note_on": 10,
+                "note_off": 20,
+            }
+        )
+        self.assertEqual(pnote["pitch"], 60)
+        self.assertEqual(pnote["note_on"], 10)
+        self.assertEqual(pnote["note_off"], 20)
+
+        # Test initialization to default values
+        self.assertEqual(pnote["track"], 0)
+        self.assertEqual(pnote["channel"], 1)
+        self.assertEqual(pnote["velocity"], 60)
+        self.assertEqual(pnote["sound_off"], 20)
+
+    def test_validate_values(self):
+        """
+        Test for _validate_values
+        """
+
+        # Invalid pitch
+        with self.assertRaises(ValueError):
+            PerformedNote(
+                {
+                    "pitch": 128,
+                    "note_on": 10,
+                    "note_off": 20,
+                }
+            )
+
+        # invalid note on
+        with self.assertRaises(ValueError):
+            PerformedNote(
+                {
+                    "pitch": 60,
+                    "note_on": -5,
+                    "note_off": 20,
+                }
+            )
+
+        # invalid note off (note off < note on)
+        with self.assertRaises(ValueError):
+            PerformedNote(
+                {
+                    "pitch": 60,
+                    "note_on": 10,
+                    "note_off": 5,
+                }
+            )
+
+        # invalid velocity (>127)
+        with self.assertRaises(ValueError):
+            PerformedNote(
+                {
+                    "pitch": 60,
+                    "note_on": 10,
+                    "note_off": 20,
+                    "velocity": 150,
+                }
+            )
+
+        # validate sound off (<note_off)
+        with self.assertRaises(ValueError):
+            PerformedNote(
+                {
+                    "pitch": 60,
+                    "note_on": 10,
+                    "note_off": 20,
+                    "sound_off": 15,
+                }
+            )
+
+        # Invalid data type for validation 
+        # (passing a list instead of a tuple or a dict)
+        pnote = PerformedNote(
+            {
+                "pitch": 60,
+                "note_on": 10,
+                "note_off": 20,
+            }
+        )
+        with self.assertRaises(ValueError):
+            pnote._validate_values(["invalid_data_type"])
+
+    def test_equality(self):
+        """
+        Test __eq__ method
+        """
+        # Notes are equal
+        pnote1 = PerformedNote(
+            {
+                "pitch": 60,
+                "note_on": 10,
+                "note_off": 20,
+            }
+        )
+        pnote2 = PerformedNote(
+            {
+                "pitch": 60,
+                "note_on": 10,
+                "note_off": 20,
+            }
+        )
+        self.assertEqual(pnote1, pnote2)
+
+        # Notes are different
+        pnote1 = PerformedNote(
+            {
+                "pitch": 60,
+                "note_on": 10,
+                "note_off": 20,
+            }
+        )
+        pnote2 = PerformedNote(
+            {
+                "pitch": 62,
+                "note_on": 10,
+                "note_off": 20,
+            }
+        )
+        self.assertNotEqual(pnote1, pnote2)
+
+    def test_ordering(self):
+        """
+        Test __le__
+        """
+        pnote1 = PerformedNote(
+            {
+                "pitch": 60,
+                "note_on": 10,
+                "note_off": 20,
+            }
+        )
+        pnote2 = PerformedNote(
+            {
+                "pitch": 60,
+                "note_on": 15,
+                "note_off": 25,
+            }
+        )
+        self.assertTrue(pnote1 < pnote2)
+        self.assertTrue(pnote1 <= pnote2)
+        self.assertTrue(pnote2 > pnote1)
+        self.assertTrue(pnote2 >= pnote1)
+
+    def test_get_and_set_item(self):
+        pnote = PerformedNote(
+            {
+                "pitch": 60,
+                "note_on": 10,
+                "note_off": 20,
+            }
+        )
+        pnote["velocity"] = 80
+        self.assertEqual(pnote["velocity"], 80)
+
+        with self.assertRaises(KeyError):
+            pnote["invalid_key"] = 100
+
+    def test_copy(self):
+        pnote = PerformedNote(
+            {
+                "pitch": 60,
+                "note_on": 10,
+                "note_off": 20,
+            }
+        )
+        pnote_copy = pnote.copy()
+        self.assertEqual(pnote, pnote_copy)
+        self.assertIsNot(pnote, pnote_copy)
+
+    def test_cannot_delete_items(self):
+        pnote = PerformedNote(
+            {
+                "pitch": 60,
+                "note_on": 10,
+                "note_off": 20,
+            }
+        )
+        with self.assertRaises(KeyError):
+            del pnote["pitch"]
+
+