Repository: mir-dataset-loaders/mirdata

Issue:
Convention for missing annotations
Some datasets have tracks with missing annotations. Right now we handle them inconsistently. For example if a beat annotation is missing for a track, some loaders return `None` and some return `BeatData(None, None)`. I'm working on a PR that will add typing to the annotations.py functions to ensure we dont use the `BeatData(None, None)` case, and updates any inconsistent loaders.

cc @drubinstein 

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mir-dataset-loaders/mirdata/pull/406?src=pr&el=h1) Report
> Merging [#406](https://codecov.io/gh/mir-dataset-loaders/mirdata/pull/406?src=pr&el=desc) (4d8a08f) into [master](https://codecov.io/gh/mir-dataset-loaders/mirdata/commit/78943401b0ea61e207c91979da21a226b452917d?el=desc) (7894340) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.


```diff
@@           Coverage Diff           @@
##           master     #406   +/-   ##
=======================================
  Coverage   97.76%   97.76%           
=======================================
  Files          33       33           
  Lines        3447     3450    +3     
=======================================
+ Hits         3370     3373    +3     
  Misses         77       77           
```


Patch:
diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst
index 5bed47a..082ca8f 100644
--- a/docs/source/tutorial.rst
+++ b/docs/source/tutorial.rst
@@ -270,6 +270,7 @@ However, to contribute to the library using remote indexes you have to add in ``
 ``download_utils.RemoteFileMetadata`` dictionary with the remote index information.
 
 .. code-block:: python
+    
     DATA = utils.LargeData("acousticbrainz_genre_index.json", remote_index=REMOTE_INDEX)
 
 
diff --git a/mirdata/annotations.py b/mirdata/annotations.py
index 0404b74..571cf52 100644
--- a/mirdata/annotations.py
+++ b/mirdata/annotations.py
@@ -1,6 +1,5 @@
 """mirdata annotation data types
 """
-
 import numpy as np
 
 
@@ -17,7 +16,7 @@ class BeatData(Annotation):
     """BeatData object
 
     Attributes:
-        times (np.ndarray or None): array of time stamps (as floats) in seconds
+        times (np.ndarray): array of time stamps (as floats) in seconds
             with positive, strictly increasing values
         positions (np.ndarray or None): array of beat positions (as ints)
             e.g. 1, 2, 3, 4
@@ -26,7 +25,7 @@ class BeatData(Annotation):
 
     def __init__(self, times, positions=None):
         validate_array_like(times, np.ndarray, float)
-        validate_array_like(positions, np.ndarray, int)
+        validate_array_like(positions, np.ndarray, int, none_allowed=True)
         validate_lengths_equal([times, positions])
         validate_times(times)
 
@@ -38,7 +37,7 @@ class SectionData(Annotation):
     """SectionData object
 
     Attributes:
-        intervals (np.ndarray or None): (n x 2) array of intervals
+        intervals (np.ndarray): (n x 2) array of intervals
             (as floats) in seconds in the form [start_time, end_time]
             times should be positive and intervals should have
             non-negative duration
@@ -48,7 +47,7 @@ class SectionData(Annotation):
 
     def __init__(self, intervals, labels=None):
         validate_array_like(intervals, np.ndarray, float)
-        validate_array_like(labels, list, str)
+        validate_array_like(labels, list, str, none_allowed=True)
         validate_lengths_equal([intervals, labels])
         validate_intervals(intervals)
 
@@ -60,20 +59,19 @@ class NoteData(Annotation):
     """NoteData object
 
     Attributes:
-        intervals (np.ndarray or None): (n x 2) array of intervals
+        intervals (np.ndarray): (n x 2) array of intervals
             (as floats) in seconds in the form [start_time, end_time]
             with positive time stamps and end_time >= start_time.
-        notes (np.ndarray or None): array of notes (as floats) in Hz
+        notes (np.ndarray): array of notes (as floats) in Hz
         confidence (np.ndarray or None): array of confidence values
             between 0 and 1
 
     """
 
     def __init__(self, intervals, notes, confidence=None):
-
         validate_array_like(intervals, np.ndarray, float)
         validate_array_like(notes, np.ndarray, float)
-        validate_array_like(confidence, np.ndarray, float)
+        validate_array_like(confidence, np.ndarray, float, none_allowed=True)
         validate_lengths_equal([intervals, notes, confidence])
         validate_intervals(intervals)
         validate_confidence(confidence)
@@ -90,17 +88,16 @@ class ChordData(Annotation):
         intervals (np.ndarray or None): (n x 2) array of intervals
             (as floats) in seconds in the form [start_time, end_time]
             with positive time stamps and end_time >= start_time.
-        labels (list or None): list chord labels (as strings)
+        labels (list): list chord labels (as strings)
         confidence (np.ndarray or None): array of confidence values
             between 0 and 1
 
     """
 
     def __init__(self, intervals, labels, confidence=None):
-
         validate_array_like(intervals, np.ndarray, float)
         validate_array_like(labels, list, str)
-        validate_array_like(confidence, np.ndarray, float)
+        validate_array_like(confidence, np.ndarray, float, none_allowed=True)
         validate_lengths_equal([intervals, labels, confidence])
         validate_intervals(intervals)
         validate_confidence(confidence)
@@ -114,9 +111,9 @@ class F0Data(Annotation):
     """F0Data object
 
     Attributes:
-        times (np.ndarray or None): array of time stamps (as floats) in seconds
+        times (np.ndarray): array of time stamps (as floats) in seconds
             with positive, strictly increasing values
-        frequencies (np.ndarray or None): array of frequency values (as floats)
+        frequencies (np.ndarray): array of frequency values (as floats)
             in Hz
         confidence (np.ndarray or None): array of confidence values
             between 0 and 1
@@ -124,10 +121,9 @@ class F0Data(Annotation):
     """
 
     def __init__(self, times, frequencies, confidence=None):
-
         validate_array_like(times, np.ndarray, float)
         validate_array_like(frequencies, np.ndarray, float)
-        validate_array_like(confidence, np.ndarray, float)
+        validate_array_like(confidence, np.ndarray, float, none_allowed=True)
         validate_lengths_equal([times, frequencies, confidence])
         validate_times(times)
         validate_confidence(confidence)
@@ -141,9 +137,9 @@ class MultiF0Data(Annotation):
     """MultiF0Data object
 
     Attributes:
-        times (np.ndarray or None): array of time stamps (as floats) in seconds
+        times (np.ndarray): array of time stamps (as floats) in seconds
             with positive, strictly increasing values
-        frequency_list (list or None): list of lists of frequency values (as floats)
+        frequency_list (list): list of lists of frequency values (as floats)
             in Hz
         confidence_list (list or None): list of lists of confidence values
             between 0 and 1
@@ -153,7 +149,7 @@ class MultiF0Data(Annotation):
     def __init__(self, times, frequency_list, confidence_list=None):
         validate_array_like(times, np.ndarray, float)
         validate_array_like(frequency_list, list, list)
-        validate_array_like(confidence_list, list, list)
+        validate_array_like(confidence_list, list, list, none_allowed=True)
         validate_lengths_equal([times, frequency_list, confidence_list])
         validate_times(times)
 
@@ -166,15 +162,14 @@ class KeyData(Annotation):
     """KeyData object
 
     Attributes:
-        intervals (np.ndarray or None): (n x 2) array of intervals
+        intervals (np.ndarray): (n x 2) array of intervals
             (as floats) in seconds in the form [start_time, end_time]
             with positive time stamps and end_time >= start_time.
-        keys (list or None): list key labels (as strings)
+        keys (list): list key labels (as strings)
 
     """
 
     def __init__(self, intervals, keys):
-
         validate_array_like(intervals, np.ndarray, float)
         validate_array_like(keys, list, str)
         validate_lengths_equal([intervals, keys])
@@ -188,19 +183,18 @@ class LyricData(Annotation):
     """LyricData object
 
     Attributes:
-        intervals (np.ndarray or None): (n x 2) array of intervals
+        intervals (np.ndarray): (n x 2) array of intervals
             (as floats) in seconds in the form [start_time, end_time]
             with positive time stamps and end_time >= start_time.
-        lyrics (list or None): list of lyrics (as strings)
+        lyrics (list): list of lyrics (as strings)
         pronunciations (list or None): list of pronunciations (as strings)
 
     """
 
     def __init__(self, intervals, lyrics, pronunciations=None):
-
         validate_array_like(intervals, np.ndarray, float)
         validate_array_like(lyrics, list, str)
-        validate_array_like(pronunciations, list, str)
+        validate_array_like(pronunciations, list, str, none_allowed=True)
         validate_lengths_equal([intervals, lyrics, pronunciations])
         validate_intervals(intervals)
 
@@ -213,20 +207,19 @@ class TempoData(Annotation):
     """TempoData object
 
     Attributes:
-        intervals (np.ndarray or None): (n x 2) array of intervals
+        intervals (np.ndarray): (n x 2) array of intervals
             (as floats) in seconds in the form [start_time, end_time]
             with positive time stamps and end_time >= start_time.
-        value (list or None): array of tempo values (as floats)
+        value (list): array of tempo values (as floats)
         confidence (np.ndarray or None): array of confidence values
             between 0 and 1
 
     """
 
     def __init__(self, intervals, value, confidence=None):
-
         validate_array_like(intervals, np.ndarray, float)
         validate_array_like(value, np.ndarray, float)
-        validate_array_like(confidence, np.ndarray, float)
+        validate_array_like(confidence, np.ndarray, float, none_allowed=True)
         validate_lengths_equal([intervals, value, confidence])
         validate_intervals(intervals)
         validate_confidence(confidence)
@@ -240,15 +233,14 @@ class EventData(Annotation):
     """TempoData object
 
     Attributes:
-        intervals (np.ndarray or None): (n x 2) array of intervals
+        intervals (np.ndarray): (n x 2) array of intervals
             (as floats) in seconds in the form [start_time, end_time]
             with positive time stamps and end_time >= start_time.
-        events (list or None): list of event labels (as strings)
+        events (list): list of event labels (as strings)
 
     """
 
     def __init__(self, intervals, events):
-
         validate_array_like(intervals, np.ndarray, float)
         validate_array_like(events, list, str)
         validate_lengths_equal([intervals, events])
@@ -258,15 +250,16 @@ class EventData(Annotation):
         self.events = events
 
 
-def validate_array_like(array_like, expected_type, expected_dtype):
+def validate_array_like(array_like, expected_type, expected_dtype, none_allowed=False):
     """Validate that array-like object is well formed
 
     If array_like is None, validation passes automatically.
 
     Args:
         array_like (array-like): object to validate
-        expected_type : expected type, either list or np.ndarray
-        expected_dtype : expected dtype
+        expected_type (type): expected type, either list or np.ndarray
+        expected_dtype (type): expected dtype
+        none_allowed (bool): if True, allows array to be None
 
     Raises:
         TypeError: if type/dtype does not match expected_type/expected_dtype
@@ -274,7 +267,10 @@ def validate_array_like(array_like, expected_type, expected_dtype):
 
     """
     if array_like is None:
-        return
+        if none_allowed:
+            return
+        else:
+            raise ValueError("array_like cannot be None")
 
     assert expected_type in [
         list,
diff --git a/mirdata/datasets/guitarset.py b/mirdata/datasets/guitarset.py
index 77c7648..128df77 100644
--- a/mirdata/datasets/guitarset.py
+++ b/mirdata/datasets/guitarset.py
@@ -398,7 +398,7 @@ def load_pitch_contour(jams_path, string_num):
     anno = anno_arr.search(data_source=str(string_num))[0]
     times, values = anno.to_event_values()
     if len(times) == 0:
-        return annotations.F0Data(None, None)
+        return None
     frequencies = [v["frequency"] for v in values]
     return annotations.F0Data(times, np.array(frequencies))
 
@@ -422,7 +422,7 @@ def load_notes(jams_path, string_num):
     anno = anno_arr.search(data_source=str(string_num))[0]
     intervals, values = anno.to_interval_values()
     if len(values) == 0:
-        return annotations.NoteData(None, None)
+        return None
     return annotations.NoteData(intervals, np.array(values))
 
 
diff --git a/mirdata/datasets/saraga_carnatic.py b/mirdata/datasets/saraga_carnatic.py
index e660ce6..8c0e5cb 100644
--- a/mirdata/datasets/saraga_carnatic.py
+++ b/mirdata/datasets/saraga_carnatic.py
@@ -380,13 +380,13 @@ def load_tempo(tempo_path):
         tempo_path (str): Local path where the tempo annotation is stored.
 
     Returns:
-        dict: 
-            {'tempo_apm': tempo in aksharas per minute (APM)
-             'tempo_bpm': tempo in beats per minute (BPM)
-             'sama_interval': median duration (in seconds) of one tāla cycle
-             'beats_per_cycle': number of beats in one cycle of the tāla
-             'subdivisions': number of aksharas per beat of the tāla
-            }
+        dict: Dictionary of tempo information with the following keys:
+
+            - tempo_apm: tempo in aksharas per minute (APM)
+            - tempo_bpm: tempo in beats per minute (BPM)
+            - sama_interval: median duration (in seconds) of one tāla cycle
+            - beats_per_cycle: number of beats in one cycle of the tāla
+            - subdivisions: number of aksharas per beat of the tāla
 
     """
     if tempo_path is None:
diff --git a/mirdata/datasets/saraga_hindustani.py b/mirdata/datasets/saraga_hindustani.py
index 6e60e9f..c1e63a5 100644
--- a/mirdata/datasets/saraga_hindustani.py
+++ b/mirdata/datasets/saraga_hindustani.py
@@ -353,14 +353,15 @@ def load_tempo(tempo_path):
     Returns:
         dict: 
             Dictionary of tempo information with the following keys:
-            * 'tempo': median tempo for the section in mātrās per minute (MPM)
-            * 'matra_interval': tempo expressed as the duration of the mātra (essentially
+            
+            - tempo: median tempo for the section in mātrās per minute (MPM)
+            - matra_interval: tempo expressed as the duration of the mātra (essentially
               dividing 60 by tempo, expressed in seconds)
-            * 'sama_interval': median duration of one tāl cycle in the section
-            * 'matras_per_cycle': indicator of the structure of the tāl, showing the number
+            - sama_interval: median duration of one tāl cycle in the section
+            - matras_per_cycle: indicator of the structure of the tāl, showing the number
               of mātrā in a cycle of the tāl of the recording
-            * 'start_time': start time of the section
-            * 'duration': duration of the section
+            - start_time: start time of the section
+            - duration: duration of the section
 
     """
     if tempo_path is None:


Test patch:
diff --git a/tests/test_annotations.py b/tests/test_annotations.py
index 4e7a26e..431c5d4 100644
--- a/tests/test_annotations.py
+++ b/tests/test_annotations.py
@@ -31,9 +31,9 @@ def test_beat_data():
     beat_data2 = annotations.BeatData(times, positions)
     assert np.allclose(beat_data2.times, times)
     assert np.allclose(beat_data2.positions, positions)
-    beat_data3 = annotations.BeatData(None, None)
-    assert beat_data3.times is None
-    assert beat_data3.positions is None
+
+    with pytest.raises(ValueError):
+        annotations.BeatData(None, None)
 
     with pytest.raises(TypeError):
         annotations.BeatData([1.0, 2.0])
@@ -57,9 +57,9 @@ def test_section_data():
     section_data2 = annotations.SectionData(intervals, labels)
     assert np.allclose(section_data2.intervals, intervals)
     assert section_data2.labels == labels
-    section_data3 = annotations.SectionData(None, None)
-    assert section_data3.intervals is None
-    assert section_data3.labels is None
+
+    with pytest.raises(ValueError):
+        annotations.SectionData(None, None)
 
     with pytest.raises(TypeError):
         annotations.SectionData([1.0, 2.0])
@@ -92,10 +92,12 @@ def test_note_data():
     assert np.allclose(note_data2.intervals, intervals)
     assert np.allclose(note_data2.notes, notes)
     assert np.allclose(note_data2.confidence, confidence)
-    note_data3 = annotations.NoteData(None, None)
-    assert note_data3.intervals is None
-    assert note_data3.notes is None
-    assert note_data3.confidence is None
+
+    with pytest.raises(ValueError):
+        annotations.NoteData(None, notes)
+
+    with pytest.raises(ValueError):
+        annotations.NoteData(intervals, None)
 
     with pytest.raises(TypeError):
         annotations.NoteData([1.0, 2.0], notes)
@@ -177,7 +179,10 @@ def test_event_data():
 
 
 def test_validate_array_like():
-    annotations.validate_array_like(None, list, str)
+    with pytest.raises(ValueError):
+        annotations.validate_array_like(None, list, str)
+
+    annotations.validate_array_like(None, list, str, none_allowed=True)
 
     with pytest.raises(TypeError):
         annotations.validate_array_like([1, 2], np.ndarray, str)
diff --git a/tests/test_guitarset.py b/tests/test_guitarset.py
index 84fd08f..ab33d8f 100644
--- a/tests/test_guitarset.py
+++ b/tests/test_guitarset.py
@@ -45,8 +45,11 @@ def test_track():
 
     run_track_tests(track, expected_attributes, expected_property_types)
 
-    assert type(track.pitch_contours["E"]) is annotations.F0Data
-    assert type(track.notes["E"]) is annotations.NoteData
+    assert track.pitch_contours["E"] is None
+    assert track.notes["E"] is None
+
+    assert isinstance(track.pitch_contours["e"], annotations.F0Data)
+    assert isinstance(track.notes["e"], annotations.NoteData)
 
 
 def test_load_beats():