Repository: cuthbertLab/music21
Pull request #1320 (2022-05-29 22:21:36)

Fix a mypy complaint.
New version of mypy is a little more stringent.


Patch:
diff --git a/.gitignore b/.gitignore
index 1d8fd0293..ece5f8677 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,7 +40,6 @@ develop-eggs
 lib
 lib64
 __pycache__
-venv
 
 # Installer logs
 pip-log.txt
diff --git a/documentation/source/usersGuide/usersGuide_26_iterators.ipynb b/documentation/source/usersGuide/usersGuide_26_iterators.ipynb
index 7c3e7c148..32ad1c46f 100644
--- a/documentation/source/usersGuide/usersGuide_26_iterators.ipynb
+++ b/documentation/source/usersGuide/usersGuide_26_iterators.ipynb
@@ -617,6 +617,7 @@
     "* :attr:`~music21.stream.iterator.StreamIterator.notes` which filters out everything but `Note` and `Chord` objects\n",
     "* :attr:`~music21.stream.iterator.StreamIterator.notesAndRests` which filters out everything except `GeneralNote` objects\n",
     "* :attr:`~music21.stream.iterator.StreamIterator.parts` which returns all the `Part` objects\n",
+    "* :attr:`~music21.stream.iterator.StreamIterator.voices`\n",
     "* :attr:`~music21.stream.iterator.StreamIterator.voices` which returns all the `Voice` objects\n",
     "* :attr:`~music21.stream.iterator.StreamIterator.spanners` which returns all the `Spanner` objects"
    ]
diff --git a/music21/chord/tables.py b/music21/chord/tables.py
index 35af16626..ddc96df6d 100644
--- a/music21/chord/tables.py
+++ b/music21/chord/tables.py
@@ -1959,7 +1959,7 @@ class Test(unittest.TestCase):
 
 # ------------------------------------------------------------------------------
 # define presented order in documentation
-_DOC_ORDER = [addressToForteName, addressToPrimeForm, seekChordTablesAddress]
+_DOC_ORDER = [addressToForteName, addressToPrimeForm, addressToForteName, seekChordTablesAddress]
 
 
 if __name__ == '__main__':
diff --git a/music21/features/jSymbolic.py b/music21/features/jSymbolic.py
index e9ef29954..956dc6dd2 100644
--- a/music21/features/jSymbolic.py
+++ b/music21/features/jSymbolic.py
@@ -618,7 +618,7 @@ class DurationOfMelodicArcsFeature(featuresModule.FeatureExtractor):
     (not counting unisons) divided by the number of times the melody
     changes direction.
 
-    Example: C D E D C D E C C
+    Example: C D E D C E D C C
     Intervals: [0] 2 2 -2 -2 2 2 -4 0
     Changes direction (equivalent to +/- sign) three times.
     There are seven non-unison (nonzero) intervals.
diff --git a/music21/key.py b/music21/key.py
index ffeb3ba96..a6d58f568 100644
--- a/music21/key.py
+++ b/music21/key.py
@@ -997,18 +997,15 @@ class Key(KeySignature, scale.DiatonicScale):
 
     def __eq__(self, other):
         '''
-        Two Keys are equal if their tonics are equal and their modes are equal.
-
-        Changed in v8: keys now compare equal regardless of the octave of their tonics:
-
-        >>> k = key.Key(pitch.Pitch('C4'))
-        >>> k2 = key.Key(pitch.Pitch('C5'))
-        >>> k == k2
-        True
+        two Keys are equal if their tonics are equal and their modes are equal
         '''
-        if not isinstance(other, Key):
-            return NotImplemented
-        return self.tonic.name == other.tonic.name and self.mode == other.mode
+        try:
+            if self.tonic == other.tonic and self.mode == other.mode:
+                return True
+            else:
+                return False
+        except AttributeError:
+            return False
 
     @property
     def relative(self) -> Key:
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index 7970f4424..4a3b9b26d 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -35,7 +35,7 @@ from music21 import pitch
 from music21 import stream
 from music21 import tempo
 
-from music21.instrument import Conductor, UnpitchedPercussion, deduplicate
+from music21.instrument import Conductor, deduplicate
 from music21.midi.percussion import MIDIPercussionException, PercussionMapper
 
 environLocal = environment.Environment('midi.translate')
@@ -419,9 +419,7 @@ def midiEventsToNote(
     return t.cast(t.Union[note.Note, note.Unpitched], nr)
 
 
-def noteToMidiEvents(
-    inputM21: t.Union[note.Note, note.Unpitched], *, includeDeltaTime=True, channel=1
-) -> t.List[t.Union['music21.midi.DeltaTime', 'music21.midi.MidiEvent']]:
+def noteToMidiEvents(inputM21: note.Note, *, includeDeltaTime=True, channel=1):
     # noinspection PyShadowingNames
     '''
     Translate a music21 Note to a list of four MIDI events --
@@ -460,7 +458,6 @@ def noteToMidiEvents(
      <music21.midi.MidiEvent NOTE_OFF, track=None, channel=9, pitch=61, velocity=0>]
 
     Changed in v7 -- made keyword-only.
-    Changed in v8 -- added support for :class:`~music21.note.Unpitched`
     '''
     from music21 import midi as midiModule
 
@@ -477,13 +474,9 @@ def noteToMidiEvents(
     me1 = midiModule.MidiEvent(track=mt)
     me1.type = midiModule.ChannelVoiceMessages.NOTE_ON
     me1.channel = channel
-
-    if isinstance(n, note.Unpitched):
-        me1.pitch = _get_unpitched_pitch_value(n)
-    else:
-        me1.pitch = n.pitch.midi
-        if not n.pitch.isTwelveTone():
-            me1.centShift = n.pitch.getCentShiftFromMidi()
+    me1.pitch = n.pitch.midi
+    if not n.pitch.isTwelveTone():
+        me1.centShift = n.pitch.getCentShiftFromMidi()
 
     # TODO: not yet using dynamics or velocity
     # volScalar = n.volume.getRealized(useDynamicContext=False,
@@ -504,8 +497,9 @@ def noteToMidiEvents(
     me2 = midiModule.MidiEvent(track=mt)
     me2.type = midiModule.ChannelVoiceMessages.NOTE_OFF
     me2.channel = channel
-    me2.pitch = me1.pitch
-    me2.centShift = me1.centShift
+    me2.pitch = n.pitch.midi
+    if not n.pitch.isTwelveTone():
+        me2.centShift = n.pitch.getCentShiftFromMidi()
 
     me2.velocity = 0  # must be zero
     eventList.append(me2)
@@ -652,9 +646,7 @@ def midiEventsToChord(
     return c
 
 
-def chordToMidiEvents(
-    inputM21: chord.ChordBase, *, includeDeltaTime=True, channel=1
-) -> t.List[t.Union['music21.midi.DeltaTime', 'music21.midi.MidiEvent']]:
+def chordToMidiEvents(inputM21, *, includeDeltaTime=True, channel=1):
     # noinspection PyShadowingNames
     '''
     Translates a :class:`~music21.chord.Chord` object to a
@@ -683,21 +675,21 @@ def chordToMidiEvents(
      <music21.midi.MidiEvent NOTE_OFF, track=None, channel=1, pitch=83, velocity=0>]
 
     Changed in v7 -- made keyword-only.
-    Changed in v8 -- added support for :class:`~music21.percussion.PercussionChord`
     '''
     from music21 import midi as midiModule
     mt = None  # midi track
-    eventList: t.List[t.Union[midiModule.DeltaTime, midiModule.MidiEvent]] = []
+    eventList = []
     c = inputM21
 
     # temporary storage for setting correspondence
-    noteOn: t.List[midiModule.MidiEvent] = []
-    noteOff: t.List[midiModule.MidiEvent] = []
+    noteOn = []
+    noteOff = []
 
     chordVolume = c.volume  # use if component volume are not defined
     hasComponentVolumes = c.hasComponentVolumes()
 
-    for i, chordComponent in enumerate(c):
+    for i in range(len(c)):
+        chordComponent = c[i]
         # pitchObj = c.pitches[i]
         # noteObj = chordComponent
         if includeDeltaTime:
@@ -711,15 +703,9 @@ def chordToMidiEvents(
         me = midiModule.MidiEvent(track=mt)
         me.type = midiModule.ChannelVoiceMessages.NOTE_ON
         me.channel = 1
-        if isinstance(chordComponent, note.Note):
-            me.pitch = chordComponent.pitch.midi
-            if not chordComponent.pitch.isTwelveTone():
-                me.centShift = chordComponent.pitch.getCentShiftFromMidi()
-        elif isinstance(chordComponent, note.Unpitched):
-            me.pitch = _get_unpitched_pitch_value(chordComponent)
-        else:  # pragma: no cover
-            raise TypeError('ChordBase can only contain Note and Unpitched as members')
-
+        me.pitch = chordComponent.pitch.midi
+        if not chordComponent.pitch.isTwelveTone():
+            me.centShift = chordComponent.pitch.getCentShiftFromMidi()
         # if 'volume' in chordComponent:
 
         if hasComponentVolumes:
@@ -738,7 +724,9 @@ def chordToMidiEvents(
         noteOn.append(me)
 
     # must create each note on in chord before each note on
-    for i, noteOnEvent in enumerate(noteOn):
+    for i in range(len(c.pitches)):
+        pitchObj = c.pitches[i]
+
         if includeDeltaTime:
             # add note off / velocity zero message
             dt = midiModule.DeltaTime(track=mt)
@@ -750,8 +738,9 @@ def chordToMidiEvents(
         me = midiModule.MidiEvent(track=mt)
         me.type = midiModule.ChannelVoiceMessages.NOTE_OFF
         me.channel = channel
-        me.pitch = noteOnEvent.pitch
-        me.centShift = noteOnEvent.centShift
+        me.pitch = pitchObj.midi
+        if not pitchObj.isTwelveTone():
+            me.centShift = pitchObj.getCentShiftFromMidi()
         me.velocity = 0  # must be zero
         eventList.append(me)
         noteOff.append(me)
@@ -765,21 +754,6 @@ def chordToMidiEvents(
     return eventList
 
 
-def _get_unpitched_pitch_value(unp: note.Unpitched) -> int:
-    unpitched_instrument = None
-    if isinstance(unp.storedInstrument, instrument.UnpitchedPercussion):
-        unpitched_instrument = unp.storedInstrument
-    else:
-        if unp._chordAttached is not None:
-            unpitched_instrument = unp._chordAttached.getContextByClass(
-                instrument.UnpitchedPercussion)
-        else:
-            unpitched_instrument = unp.getContextByClass(instrument.UnpitchedPercussion)
-    if unpitched_instrument is not None and unpitched_instrument.percMapPitch is not None:
-        return unpitched_instrument.percMapPitch
-    return 60  # e.g. lossy instrument recognition from musicxml
-
-
 # ------------------------------------------------------------------------------
 def instrumentToMidiEvents(inputM21,
                            includeDeltaTime=True,
@@ -1270,7 +1244,6 @@ def elementToMidiEventList(
     '''
     # TODO: this is the best use of the switch statement when minimum Python
     #    version is 3.10
-    sub: t.Optional[t.List[t.Union['music21.midi.DeltaTime', 'music21.midi.MidiEvent']]]
     if isinstance(el, note.Rest):
         return None
     elif isinstance(el, note.Note):
@@ -1278,13 +1251,10 @@ def elementToMidiEventList(
         # using this property here is easier than using the above conversion
         # methods, as we do not need to know what the object is
         sub = noteToMidiEvents(el, includeDeltaTime=False)
-    elif isinstance(el, note.Unpitched):
-        sub = noteToMidiEvents(el, includeDeltaTime=False, channel=10)
+    # TODO: unpitched
     elif isinstance(el, chord.Chord):
         # TODO: skip Harmony unless showAsChord
         sub = chordToMidiEvents(el, includeDeltaTime=False)
-    elif isinstance(el, percussion.PercussionChord):
-        sub = chordToMidiEvents(el, includeDeltaTime=False, channel=10)
     elif isinstance(el, dynamics.Dynamic):
         return None  # dynamics have already been applied to notes
     elif isinstance(el, meter.TimeSignature):
@@ -2429,24 +2399,15 @@ def packetStorageFromSubstreamList(
         # get a first instrument; iterate over rest
         instrumentStream = subs.getElementsByClass(instrument.Instrument)
 
-        instObj = None
         # if there is an Instrument object at the start, make instObj that instrument
         # this may be a Conductor object if prepareStreamForMidi() was run
         if instrumentStream and subs.elementOffset(instrumentStream[0]) == 0:
             instObj = instrumentStream[0]
-        # If there is no such instrument, or a really lousy generic one without a midiProgram,
-        # check if this stream has any Unpitched objects. Make instObj an UnpitchedPercussion.
-        if (instrumentStream is None
-            # lousy instrument e.g. "music21.instrument.Instrument: ch10 jazzdrums1"
-            or (instObj is not None and instObj.midiProgram is None)
-            ) and subs.getElementsByClass(
-                (note.Unpitched, percussion.PercussionChord)):
-            # This dummy instance will be enough to get a channel 10 default in
-            # assignPacketsToChannels(). Later, the proper instrument in the stream will be read
-            instObj = UnpitchedPercussion()
         elif trackId == 0 and not subs.notesAndRests:
             # maybe prepareStreamForMidi() wasn't run; create Conductor instance
             instObj = Conductor()
+        else:
+            instObj = None
 
         trackPackets = streamToPackets(subs, trackId=trackId, addStartDelay=addStartDelay)
         # store packets in dictionary; keys are trackIds
@@ -2475,9 +2436,7 @@ def updatePacketStorageWithChannelInfo(
                 initCh = channelByInstrument[None]
             except KeyError:  # pragma: no cover
                 initCh = 1  # fallback, should not happen.
-        elif isinstance(instObj, instrument.UnpitchedPercussion):
-            initCh = 10
-        elif isinstance(instObj, instrument.Conductor):
+        elif 'Conductor' in instObj.classes:
             initCh = None
         else:  # keys are midi program
             initCh = channelByInstrument[instObj.midiProgram]
@@ -4057,48 +4016,6 @@ class Test(unittest.TestCase):
         meta_event_pairs = getMetaEvents([(DUMMY_DELTA_TIME, event2)])
         self.assertEqual(meta_event_pairs[0][1].midiProgram, 53)
 
-    def testExportUnpitched(self):
-        from music21 import midi as midiModule
-
-        m = stream.Measure(
-            [instrument.BassDrum(),
-            note.Unpitched(),
-            percussion.PercussionChord([note.Unpitched(), note.Unpitched()])]
-        )
-        trks = streamHierarchyToMidiTracks(m)
-        bd_trk = trks[1]
-
-        self.assertTrue({ev.channel for ev in bd_trk.events}, {10})
-        note_ons = [
-            ev for ev in bd_trk.events if ev.type is midiModule.ChannelVoiceMessages.NOTE_ON]
-        self.assertEqual(len(note_ons), 3)
-        self.assertEqual({ev.pitch for ev in note_ons}, {35})
-
-        # Replace the BassDrum with a vague instrument (lossy import)
-        m.pop(0)
-        m.insert(0, instrument.Instrument())
-
-        trks = streamHierarchyToMidiTracks(m)
-        drum_trk = trks[1]
-
-        self.assertTrue({ev.channel for ev in drum_trk.events}, {10})
-        note_ons = [
-            ev for ev in drum_trk.events if ev.type is midiModule.ChannelVoiceMessages.NOTE_ON]
-        self.assertEqual(len(note_ons), 3)
-        self.assertEqual({ev.pitch for ev in note_ons}, {60})  # fallback
-
-        # Change the stored instrument: affects that note only
-        m.notes.first().storedInstrument = instrument.Agogo()
-
-        trks = streamHierarchyToMidiTracks(m)
-        mixed_trk = trks[1]
-
-        self.assertTrue({ev.channel for ev in mixed_trk.events}, {10})
-        note_ons = [
-            ev for ev in mixed_trk.events if ev.type is midiModule.ChannelVoiceMessages.NOTE_ON]
-        self.assertEqual(len(note_ons), 3)
-        self.assertEqual([ev.pitch for ev in note_ons], [67, 60, 60])
-
 
 # ------------------------------------------------------------------------------
 _DOC_ORDER = [streamToMidiFile, midiFileToStream]
diff --git a/music21/note.py b/music21/note.py
index b7fe63057..c25300bf1 100644
--- a/music21/note.py
+++ b/music21/note.py
@@ -1776,7 +1776,6 @@ class Unpitched(NotRest):
 
     def __init__(self, displayName=None, **keywords):
         super().__init__(**keywords)
-        self._chordAttached: t.Optional['music21.percussion.PercussionChord'] = None
 
         self.displayStep: StepName = 'B'
         self.displayOctave: int = 4
diff --git a/music21/scale/intervalNetwork.py b/music21/scale/intervalNetwork.py
index b784facc4..5c1202c74 100644
--- a/music21/scale/intervalNetwork.py
+++ b/music21/scale/intervalNetwork.py
@@ -72,8 +72,7 @@ class Direction(enum.Enum):
         return 'Direction.' + self.name
 
 
-CacheKey = t.Tuple[
-    t.Union[int, Terminus], str, t.Union[str, None], t.Union[str, None], bool, t.Optional[bool]]
+CacheKey = t.Tuple[t.Union[int, Terminus], str, t.Union[str, None], t.Union[str, None], bool]
 
 
 def _gte(a, b):
@@ -1342,7 +1341,6 @@ class IntervalNetwork:
         minPitch: t.Optional[pitch.Pitch],
         maxPitch: t.Optional[pitch.Pitch],
         includeFirst: bool,
-        reverse: t.Optional[bool] = None,  # only meaningful for descending
     ) -> CacheKey:
         '''
         Return key for caching based on critical components.
@@ -1360,9 +1358,7 @@ class IntervalNetwork:
                 pitchReference.nameWithOctave,
                 minKey,
                 maxKey,
-                includeFirst,
-                reverse,
-                )
+                includeFirst)
 
     def realizeAscending(
         self,
@@ -1639,9 +1635,7 @@ class IntervalNetwork:
                                    pitchRef,
                                    minPitchObj,
                                    maxPitchObj,
-                                   includeFirst,
-                                   reverse,
-                                   )
+                                   includeFirst)
             if ck in self._descendingCache:
                 return self._descendingCache[ck]
 
@@ -1919,10 +1913,8 @@ class IntervalNetwork:
             mergedPitches, mergedNodes = pre + post, preNodeId + postNodeId
 
         if reverse:
-            # Make new objects, because this value might be cached in intervalNetwork's
-            # _descendingCache, and mutating it would be dangerous.
-            mergedPitches = list(reversed(mergedPitches))
-            mergedNodes = list(reversed(mergedNodes))
+            mergedPitches.reverse()
+            mergedNodes.reverse()
 
         return mergedPitches, mergedNodes
 
diff --git a/music21/search/lyrics.py b/music21/search/lyrics.py
index 33f8e15d7..e127af47a 100644
--- a/music21/search/lyrics.py
+++ b/music21/search/lyrics.py
@@ -371,7 +371,9 @@ class LyricSearcher:
     #     return lineBreakStart
 
     def _reSearch(self, r: re.Pattern) -> t.List[SearchMatch]:
-        locations = []
+        locations: t.List[SearchMatch] = []
+        if self._indexText is None:
+            return locations
         for m in r.finditer(self._indexText):
             absoluteFoundPos, absoluteEndPos = m.span()
             matchText = m.group(0)


Test patch:
diff --git a/documentation/source/developerReference/testing.ipynb b/documentation/source/developerReference/testing.ipynb
index 9a17d5083..e95d5f713 100644
--- a/documentation/source/developerReference/testing.ipynb
+++ b/documentation/source/developerReference/testing.ipynb
@@ -210,7 +210,7 @@
     "\n",
     "    https://coveralls.io/github/cuthbertLab/music21\n",
     "\n",
-    "While 100% code coverage is impossible, increasing the coverage percentage with each new contribution is essential.  We're at 93% code coverage and get the green badge of pride on our README.md file, and we like that!\n",
+    "While 100% code coverage is impossible, increasing the coverage percentage with each new contribution is essential.  We're at 92% code coverage and get the green badge of pride on our README.md file, and we like that!\n",
     "\n",
     "If something should be impossible to trigger, but the code is being maintained for some reason, you can add \"`# pragma: no cover`\" to the line or to the fork, but this is generally a no-no."
    ]
diff --git a/music21/scale/test_intervalNetwork.py b/music21/scale/test_intervalNetwork.py
index ec509fdbd..f4c7e6208 100644
--- a/music21/scale/test_intervalNetwork.py
+++ b/music21/scale/test_intervalNetwork.py
@@ -594,20 +594,6 @@ class Test(unittest.TestCase):
                 getNeighbor=Direction.DESCENDING)),
             'A-4')
 
-    def test_realize_descending_reversed_cached(self):
-        net = IntervalNetwork()
-        net.fillMelodicMinor()
-
-        descending_melodic_minor, _ = net.realizeDescending(
-            'C4', minPitch='C4', maxPitch='C5', reverse=False)
-        self.assertEqual(descending_melodic_minor[0].nameWithOctave, 'B-4')
-        self.assertEqual(descending_melodic_minor[-1].nameWithOctave, 'C4')
-
-        descending_melodic_minor_reversed, _ = net.realizeDescending(
-            'C4', minPitch='C4', maxPitch='C5', reverse=True)
-        self.assertEqual(descending_melodic_minor_reversed[0].nameWithOctave, 'C4')  # was B-4
-        self.assertEqual(descending_melodic_minor_reversed[-1].nameWithOctave, 'B-4')  # was C4
-
 
 # ------------------------------------------------------------------------------
 if __name__ == '__main__':
diff --git a/music21/scale/test_scale_main.py b/music21/scale/test_scale_main.py
index 7c346e5c5..0e98f543b 100644
--- a/music21/scale/test_scale_main.py
+++ b/music21/scale/test_scale_main.py
@@ -302,13 +302,16 @@ class Test(unittest.TestCase):
         self.assertEqual(self.pitchOut(mm.getPitches('c1', 'c3', direction=Direction.DESCENDING)),
                          '[C3, B2, A2, G2, F2, E2, D2, C2, B1, A1, G1, F1, E1, D1, C1]')
 
+        # TODO: this shows a problem with a bidirectional scale: we are
+        # always starting at the tonic and moving up or down; so this is still
+        # giving a descended portion, even though an ascending portion was requested
         # noinspection PyArgumentList
         self.assertEqual(self.pitchOut(mm.getPitches('c1', 'c3', direction=Direction.ASCENDING)),
                          '[C1, D1, E1, F#1, G#1, A1, B1, C2, D2, E2, F#2, G#2, A2, B2, C3]')
 
         # noinspection PyArgumentList
         self.assertEqual(self.pitchOut(mm.getPitches('c1', 'c3', direction=Direction.DESCENDING)),
-                         '[C3, B2, A2, G2, F2, E2, D2, C2, B1, A1, G1, F1, E1, D1, C1]')
+                         '[C1, D1, E1, F1, G1, A1, B1, C2, D2, E2, F2, G2, A2, B2, C3]')
 
         # noinspection PyArgumentList
         self.assertEqual(self.pitchOut(mm.getPitches('a5', 'a6', direction=Direction.ASCENDING)),
@@ -900,19 +903,6 @@ Franck Jedrzejewski continued fractions approx. of 12-tet
         with self.assertRaises(intervalNetwork.IntervalNetworkException):
             e.deriveRanked(['C4', 'E4', 'G4'], comparisonAttribute='name')
 
-    def test_getPitches_multiple_times(self):
-        """Worth testing because we found cached lists being mutated."""
-        c_maj = scale.MajorScale('C')
-
-        for i in range(3):  # catch both even and odd states
-            with self.subTest(iterations=i):
-                # due to a tight coupling of direction and reverse, this sets reverse=True
-                # when calling getRealization()
-                post = c_maj.getPitches(direction=Direction.DESCENDING)
-
-                self.assertEqual(post[0].nameWithOctave, 'C5')
-                self.assertEqual(post[-1].nameWithOctave, 'C4')
-
 
 # ------------------------------------------------------------------------------
 if __name__ == '__main__':