Repository: cuthbertLab/music21
Pull request #1076 (2021-08-04 04:34:41)

[misc] Optimizations related to recent PRs
A miscellany branch mostly related to recent PRs.

Highlights involve speeding up makeRests() and MIDI parsing--details in commit messages.

jacobtylerwalls: Reverted one of the commits I wasn't sure about. I guess tests were passing locally because of not bumping a version number or something.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/41888042/badge)](https://coveralls.io/builds/41888042)

Coverage increased (+0.05%) to 93.064% when pulling **61307f59b1f0989e4393d6dca16a09d56fb670f5 on jacobtylerwalls:august-miscellany** into **8343855f7b3aee3aa4e66010017e3cbbf87295d3 on cuthbertLab:master**.

mscuthbert: Everything except the MIDI instrument translate i was able to review immediately and it's good to go.  It'll take me a little more time to review that part of the PR.  If you want to cherry-pick that to a separate PR then feel free to merge it.  Otherwise leave open a bit longer and I'll try to find time soon.
jacobtylerwalls: Great, I moved it to #1078

Patch:
diff --git a/documentation/source/usersGuide/usersGuide_27_graceNotes.ipynb b/documentation/source/usersGuide/usersGuide_27_graceNotes.ipynb
index 53851e51b..43a9fd80c 100644
--- a/documentation/source/usersGuide/usersGuide_27_graceNotes.ipynb
+++ b/documentation/source/usersGuide/usersGuide_27_graceNotes.ipynb
@@ -583,7 +583,7 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "(Alternatively, this could be done in one step if `mCopy.recurse().notes` was replaced with `list(mCopy.recurse().notes)`, but better to be explicit about what we are doing then implicit.) "
+    "(Alternatively, this could be done in one step if `mCopy.recurse().notes` was replaced with `list(mCopy.recurse().notes)`, but better to be explicit about what we are doing than implicit.) "
    ]
   },
   {
diff --git a/music21/analysis/reduction.py b/music21/analysis/reduction.py
index 11bb305fe..7b4d69233 100644
--- a/music21/analysis/reduction.py
+++ b/music21/analysis/reduction.py
@@ -220,6 +220,7 @@ class ScoreReduction:
             s = stream.Score()
             s.insert(0, copy.deepcopy(value))
             self._score = s
+        self._score.setDerivationMethod('ScoreReduction', recurse=True)
 
     def _getScore(self):
         return self._score
diff --git a/music21/graph/primitives.py b/music21/graph/primitives.py
index fdfff682e..e7fb6ff80 100644
--- a/music21/graph/primitives.py
+++ b/music21/graph/primitives.py
@@ -1661,7 +1661,7 @@ class TestExternal(unittest.TestCase):
         a.data = data
         a.process()
 
-    def writeAllGraphs(self):
+    def x_test_writeAllGraphs(self):
         '''
         Write a graphic file for all graphs,
         naming them after the appropriate class.
@@ -1710,7 +1710,7 @@ class TestExternal(unittest.TestCase):
             environLocal.printDebug(['writing fp:', fp])
             obj.write(fp)
 
-    def writeGraphColorGrid(self):
+    def x_test_writeGraphColorGrid(self):
         # this is temporary
         a = GraphColorGrid(doneAction=None)
         data = [['#525252', '#5f5f5f', '#797979', '#858585', '#727272', '#6c6c6c',
@@ -1739,7 +1739,7 @@ class TestExternal(unittest.TestCase):
 
         a.write(fp)
 
-    def writeGraphingDocs(self):
+    def x_test_writeGraphingDocs(self):
         '''
         Write graphing examples for the docs
         '''
diff --git a/music21/instrument.py b/music21/instrument.py
index 0f21fbc51..7a97e3bde 100644
--- a/music21/instrument.py
+++ b/music21/instrument.py
@@ -63,7 +63,7 @@ def unbundleInstruments(streamIn, *, inPlace=False):
     if inPlace is True:
         s = streamIn
     else:
-        s = copy.deepcopy(streamIn)
+        s = streamIn.coreCopyAsDerivation('unbundleInstruments')
 
     for thisObj in s:
         if 'Unpitched' in thisObj.classes:
@@ -101,7 +101,7 @@ def bundleInstruments(streamIn, *, inPlace=False):
     if inPlace is True:
         s = streamIn
     else:
-        s = copy.deepcopy(streamIn)
+        s = streamIn.coreCopyAsDerivation('bundleInstruments')
 
     lastInstrument = None
 
@@ -1849,7 +1849,7 @@ def deduplicate(s: Stream, inPlace: bool = False) -> Stream:
     if inPlace:
         returnObj = s
     else:
-        returnObj = copy.deepcopy(s)
+        returnObj = s.coreCopyAsDerivation('instrument.deduplicate')
 
     if not returnObj.hasPartLikeStreams():
         substreams = [returnObj]
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index f84571167..4ee075a20 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -22,6 +22,7 @@ import warnings
 from music21 import chord
 from music21 import common
 from music21 import defaults
+from music21 import duration
 from music21 import note
 from music21 import exceptions21
 from music21 import environment
@@ -150,7 +151,6 @@ def ticksToDuration(ticks, ticksPerQuarter=None, inputM21DurationObject=None):
 
     '''
     if inputM21DurationObject is None:
-        from music21 import duration
         d = duration.Duration()
     else:
         d = inputM21DurationObject
@@ -268,7 +268,7 @@ def music21ObjectToMidiFile(
 # ------------------------------------------------------------------------------
 # Notes
 
-def midiEventsToNote(eventList, ticksPerQuarter=None, inputM21=None):
+def midiEventsToNote(eventList, ticksPerQuarter=None, inputM21=None) -> note.Note:
     # noinspection PyShadowingNames
     '''
     Convert from a list of midi.DeltaTime and midi.MidiEvent objects to a music21 Note.
@@ -332,11 +332,6 @@ def midiEventsToNote(eventList, ticksPerQuarter=None, inputM21=None):
     94
 
     '''
-    if inputM21 is None:
-        n = note.Note()
-    else:
-        n = inputM21
-
     if ticksPerQuarter is None:
         ticksPerQuarter = defaults.ticksPerQuarter
 
@@ -355,18 +350,27 @@ def midiEventsToNote(eventList, ticksPerQuarter=None, inputM21=None):
     else:
         raise TranslateException(f'cannot handle MIDI event list in the form: {eventList!r}')
 
-    n.pitch.midi = eOn.pitch
-    n.volume.velocity = eOn.velocity
-    n.volume.velocityIsRelative = False  # not relative coming from MIDI
-    # n._midiVelocity = eOn.velocity
     # here we are handling an issue that might arise with double-stemmed notes
     if (tOff - tOn) != 0:
-        ticksToDuration(tOff - tOn, ticksPerQuarter, n.duration)
+        if inputM21 is None:
+            n = note.Note(duration=ticksToDuration(tOff - tOn, ticksPerQuarter))
+        else:
+            n = inputM21
+            n.duration = ticksToDuration(tOff - tOn, ticksPerQuarter, n.duration)
     else:
         # environLocal.printDebug(['cannot translate found midi event with zero duration:', eOn, n])
         # for now, substitute grace note
+        if inputM21 is None:
+            n = note.Note()
+        else:
+            n = inputM21
         n.getGrace(inPlace=True)
 
+    n.pitch.midi = eOn.pitch
+    n.volume.velocity = eOn.velocity
+    n.volume.velocityIsRelative = False  # not relative coming from MIDI
+    # n._midiVelocity = eOn.velocity
+
     return n
 
 
@@ -523,11 +527,6 @@ def midiEventsToChord(eventList, ticksPerQuarter=None, inputM21=None):
     tOn: int = 0  # ticks
     tOff: int = 0  # ticks
 
-    if inputM21 is None:
-        c = chord.Chord()
-    else:
-        c = inputM21
-
     if ticksPerQuarter is None:
         ticksPerQuarter = defaults.ticksPerQuarter
 
@@ -570,16 +569,26 @@ def midiEventsToChord(eventList, ticksPerQuarter=None, inputM21=None):
     else:
         raise TranslateException(f'fewer than 4 events provided to midiEventsToChord: {eventList}')
 
-    c.pitches = pitches
-    c.volume = volumes  # can set a list to volume property
     # can simply use last-assigned pair of tOff, tOn
     if (tOff - tOn) != 0:
-        ticksToDuration(tOff - tOn, ticksPerQuarter, c.duration)
+        if inputM21 is None:
+            c = chord.Chord(duration=ticksToDuration(tOff - tOn, ticksPerQuarter))
+        else:
+            c = inputM21
+            c.duration = ticksToDuration(tOff - tOn, ticksPerQuarter, c.duration)
     else:
         environLocal.warn(['midi chord with zero duration will be treated as grace',
                             eventList, c])
         # for now, get grace
+        if inputM21 is None:
+            c = chord.Chord()
+        else:
+            c = inputM21
         c.getGrace(inPlace=True)
+
+    c.pitches = pitches
+    c.volume = volumes  # can set a list to volume property
+
     return c
 
 
@@ -1924,9 +1933,7 @@ def midiTrackToStream(
             # execute; chordSub will be None
             if chordSub is not None:
                 # composite.append(chordSub)
-                # create a chord here
-                c = chord.Chord()
-                midiEventsToChord(chordSub, ticksPerQuarter, c)
+                c = midiEventsToChord(chordSub, ticksPerQuarter)
                 o = notes[i][0][0] / ticksPerQuarter
                 c.midiTickStart = notes[i][0][0]
 
@@ -1935,9 +1942,7 @@ def midiTrackToStream(
                 chordSub = None
             else:  # just append the note, chordSub is None
                 # composite.append(notes[i])
-                # create a note here
-                n = note.Note()
-                midiEventsToNote(notes[i], ticksPerQuarter, n)
+                n = midiEventsToNote(notes[i], ticksPerQuarter)
                 # the time is the first value in the first pair
                 # need to round, as floating point error is likely
                 o = notes[i][0][0] / ticksPerQuarter
@@ -1949,12 +1954,11 @@ def midiTrackToStream(
             i += 1
 
     elif len(notes) == 1:  # rare case of just one note
-        n = note.Note()
-        midiEventsToNote(notes[0], ticksPerQuarter, n)
+        n = midiEventsToNote(notes[0], ticksPerQuarter)
         # the time is the first value in the first pair
         # need to round, as floating point error is likely
         o = notes[0][0][0] / ticksPerQuarter
-        n.midiTickStart = notes[i][0][0]
+        n.midiTickStart = notes[0][0][0]
         s.coreInsert(o, n)
 
     s.coreElementsChanged()
@@ -1974,11 +1978,14 @@ def midiTrackToStream(
     if conductorPart is not None:
         insertConductorEvents(conductorPart, s, isFirst=isFirst)
 
-    # Only make measures if time signatures have been inserted
-    s.makeMeasures(inPlace=True)
-    for m in s.getElementsByClass(stream.Measure):
-        if voicesRequired:
-            m.makeVoices(inPlace=True, fillGaps=True)
+    # Only make measures once time signatures have been inserted
+    s.makeMeasures(
+        meterStream=conductorPart['TimeSignature'].stream() if conductorPart else None,
+        inPlace=True)
+    if voicesRequired:
+        for m in s.getElementsByClass(stream.Measure):
+            # Gaps will be filled by makeRests, below, which now recurses
+            m.makeVoices(inPlace=True, fillGaps=False)
     s.makeTies(inPlace=True)
     # always need to fill gaps, as rests are not found in any other way
     s.makeRests(inPlace=True, fillGaps=True, timeRangeFromBarDuration=True)
@@ -2018,10 +2025,10 @@ def prepareStreamForMidi(s) -> stream.Stream:
     '''
     from music21 import volume
 
-    if s.recurse().stream().hasMeasures():
+    if s[stream.Measure]:
         s = s.expandRepeats()  # makes a deep copy
     else:
-        s = copy.deepcopy(s)
+        s = s.coreCopyAsDerivation('prepareStreamForMidi')
 
     conductor = conductorStream(s)
 
@@ -2097,15 +2104,13 @@ def conductorStream(s: stream.Stream) -> stream.Part:
     conductorPart.insert(0, Conductor())
 
     for klass in ('MetronomeMark', 'TimeSignature', 'KeySignature'):
-        events = s.flat.getElementsByClass(klass)
         lastOffset = -1
-        for el in events:
-            o = events.srcStream.elementOffset(el)
-            s.remove(el, recurse=True)
+        for el in s[klass]:
             # Don't overwrite an event of the same class at this offset
-            if o > lastOffset:
-                conductorPart.coreInsert(o, el)
-            lastOffset = o
+            if el.offset > lastOffset:
+                conductorPart.coreInsert(el.offset, el)
+            lastOffset = el.offset
+            s.remove(el, recurse=True)
 
     conductorPart.coreElementsChanged()
 
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 050e8e405..876af4013 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -383,9 +383,9 @@ class XMLParserBase:
             if mxValue is None:
                 continue
 
-            if m21Name in xmlObjects.STYLE_ATTRIBUTES_STR_NONE_TO_NONE and mxValue == 'none':
+            if mxValue == 'none' and m21Name in xmlObjects.STYLE_ATTRIBUTES_STR_NONE_TO_NONE:
                 mxValue = None
-            if m21Name in xmlObjects.STYLE_ATTRIBUTES_YES_NO_TO_BOOL:
+            elif m21Name in xmlObjects.STYLE_ATTRIBUTES_YES_NO_TO_BOOL:
                 mxValue = xmlObjects.yesNoToBoolean(mxValue)
 
             try:
diff --git a/music21/pitch.py b/music21/pitch.py
index 832312619..0fef25a78 100644
--- a/music21/pitch.py
+++ b/music21/pitch.py
@@ -233,41 +233,41 @@ def _convertPsToStep(ps) -> Tuple[str, 'Accidental', 'Microtone', int]:
     '''
     name = ''
 
-    # rounding here is essential
-    ps = round(ps, PITCH_SPACE_SIG_DIGITS)
-    pcReal = ps % 12
-    # micro here will be between 0 and 1
-    pc, micro = divmod(pcReal, 1)
-
-    # environLocal.printDebug(['_convertPsToStep(): post divmod',  'ps', repr(ps),
-    #    'pcReal', repr(pcReal), 'pc', repr(pc), 'micro', repr(micro)])
-
-    # if close enough to a quarter tone
-    if round(micro, 1) == 0.5:
-        # if can round to 0.5, than this is a quarter-tone accidental
-        alter = 0.5
-        # need to find microtonal alteration around this value
-        # of alter is 0.5 and micro is 0.7 than  micro should be 0.2
-        # of alter is 0.5 and micro is 0.4 than  micro should be -0.1
-        micro = micro - alter
-
-    # if greater than 0.5
-    elif 0.25 < micro < 0.75:
-        alter = 0.5
-        micro = micro - alter
-    # if closer to 1, than go to the higher alter and get negative micro
-    elif 0.75 <= micro < 1:
-        alter = 1
-        micro = micro - alter
-    # not greater than 0.25
-    elif micro > 0:
-        alter = 0
-        # micro = micro  # no change necessary
-    else:
+    if isinstance(ps, int):
+        pc = ps % 12
         alter = 0
         micro = 0
-
-    pc = int(pc)
+    else:
+        # rounding here is essential
+        ps = round(ps, PITCH_SPACE_SIG_DIGITS)
+        # micro here will be between 0 and 1
+        pcReal = ps % 12
+        pc, micro = divmod(pcReal, 1)
+        pc = int(pc)
+
+        # if close enough to a quarter tone
+        if round(micro, 1) == 0.5:
+            # if can round to 0.5, than this is a quarter-tone accidental
+            alter = 0.5
+            # need to find microtonal alteration around this value
+            # of alter is 0.5 and micro is 0.7 than  micro should be 0.2
+            # of alter is 0.5 and micro is 0.4 than  micro should be -0.1
+            micro = micro - alter
+        # if greater than 0.5
+        elif 0.25 < micro < 0.75:
+            alter = 0.5
+            micro = micro - alter
+        # if closer to 1, than go to the higher alter and get negative micro
+        elif 0.75 <= micro < 1:
+            alter = 1
+            micro = micro - alter
+        # not greater than 0.25
+        elif micro > 0:
+            alter = 0
+            # micro = micro  # no change necessary
+        else:
+            alter = 0
+            micro = 0
 
     # environLocal.printDebug(['_convertPsToStep(): post', 'alter', alter,
     #    'micro', micro, 'pc', pc])
diff --git a/music21/repeat.py b/music21/repeat.py
index 19f15561b..6999fc621 100644
--- a/music21/repeat.py
+++ b/music21/repeat.py
@@ -426,7 +426,7 @@ def insertRepeatEnding(s, start, end, endingNumber=1, *, inPlace=False):
     '''
 
     if not inPlace:
-        s = copy.deepcopy(s)
+        s.coreCopyAsDerivation('insertRepeatEnding')
 
     if s is None:
         return None  # or raise an exception!
@@ -493,7 +493,7 @@ def insertRepeat(s, start, end, *, inPlace=False):
         return None
 
     if not inPlace:
-        s = copy.deepcopy(s)
+        s.coreCopyAsDerivation('insertRepeat')
 
     if not s.hasMeasures():
         for part in s.parts:
@@ -565,7 +565,7 @@ def deleteMeasures(s, toDelete, *, inPlace=False, correctMeasureNumbers=True):
         return None
 
     if not inPlace:
-        s = copy.deepcopy(s)
+        s = s.coreCopyAsDerivation('deleteMeasures')
 
     if s.hasMeasures():
         for mNumber in toDelete:
@@ -773,7 +773,7 @@ class Expander:
         # need to copy source measures, as may later measures before copying
         # them, and this can result in orphaned spanners
         if deepcopy is not False:
-            srcStream = copy.deepcopy(self._srcMeasureStream)
+            srcStream = self._srcMeasureStream.coreCopyAsDerivation('expandRepeats')
         else:
             srcStream = self._srcMeasureStream
 
@@ -2457,7 +2457,7 @@ class RepeatFinder:
         if inPlace:
             s = self.s
         else:
-            s = copy.deepcopy(self.s)
+            s = self.s.coreCopyAsDerivation('RepeatFinder.simplify')
 
         if s is None:
             raise NoInternalStreamException(
diff --git a/music21/search/serial.py b/music21/search/serial.py
index fedbed578..61616bd83 100644
--- a/music21/search/serial.py
+++ b/music21/search/serial.py
@@ -2138,7 +2138,7 @@ def labelTransposedSegments(inputStream, segmentDict, reps='skipConsecutive', in
     >>> len(labeledSC.parts[0].getElementsByClass(spanner.Line))
     2
     '''
-    streamCopy = copy.deepcopy(inputStream)
+    streamCopy = inputStream.coreCopyAsDerivation('labelTransposedSegments')
     segmentList = [segmentDict[label] for label in segmentDict]
     segmentsToLabel = TransposedSegmentMatcher(streamCopy, reps, includeChords).find(segmentList)
     return _labelGeneral(segmentsToLabel, streamCopy, segmentDict, reps, includeChords)
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 6356ed065..ce68b9ccd 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -1575,7 +1575,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         for i, target in enumerate(targetList):
             try:
                 indexInStream = self.index(target)
-            except StreamException:
+            except StreamException as se:
+                if not isinstance(target, base.Music21Object):
+                    raise TypeError(f'{target} is not a Music21Object; got {type(target)}') from se
                 if recurse is True:
                     for s in self.recurse(streamsOnly=True):
                         try:
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index ab47aeca8..e4bcbed57 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -113,7 +113,7 @@ def makeBeams(
 
     # environLocal.printDebug(['calling Stream.makeBeams()'])
     if not inPlace:  # make a copy
-        returnObj: stream.Stream = copy.deepcopy(s)
+        returnObj: stream.Stream = s.coreCopyAsDerivation('makeBeams')
     else:
         returnObj: stream.Stream = s
 
@@ -676,6 +676,7 @@ def makeMeasures(
             m.clef = clef.bestClef(m, recurse=True)
 
     if not inPlace:
+        post.setDerivationMethod('makeMeasures', recurse=True)
         return post  # returns a new stream populated w/ new measure streams
     else:  # clear the stored elements list of this Stream and repopulate
         # with Measures created above
@@ -824,8 +825,7 @@ def makeRests(
     from music21 import stream
 
     if not inPlace:  # make a copy
-        returnObj = copy.deepcopy(s)
-        returnObj.derivation.method = 'makeRests'
+        returnObj = s.coreCopyAsDerivation('makeRests')
     else:
         returnObj = s
 
@@ -1156,8 +1156,7 @@ def makeTies(
     # environLocal.printDebug(['calling Stream.makeTies()'])
 
     if not inPlace:  # make a copy
-        returnObj = copy.deepcopy(s)
-        returnObj.derivation.method = 'makeTies'
+        returnObj = s.coreCopyAsDerivation('makeTies')
     else:
         returnObj = s
     if not returnObj:
@@ -1362,8 +1361,7 @@ def makeTupletBrackets(s, *, inPlace=False):
     else:
         # Stream, as it should be...
         if not inPlace:  # make a copy
-            returnObj = copy.deepcopy(s)
-            returnObj.derivation.method = 'makeTupletBrackets'
+            returnObj = s.coreCopyAsDerivation('makeTupletBrackets')
         else:
             returnObj = s
 
diff --git a/music21/variant.py b/music21/variant.py
index 91b49b5da..ddd451881 100644
--- a/music21/variant.py
+++ b/music21/variant.py
@@ -228,7 +228,7 @@ def mergeVariantScores(aScore, vScore, variantName='variant', *, inPlace=False):
     if inPlace is True:
         returnObj = aScore
     else:
-        returnObj = copy.deepcopy(aScore)
+        returnObj = aScore.coreCopyAsDerivation('mergeVariantScores')
 
     for returnPart, vPart in zip(returnObj.parts, vScore.parts):
         mergeVariantMeasureStreams(returnPart, vPart, variantName, inPlace=True)
@@ -391,7 +391,7 @@ def mergeVariantMeasureStreams(streamX, streamY, variantName='variant', *, inPla
     if inPlace is True:
         returnObj = streamX
     else:
-        returnObj = copy.deepcopy(streamX)
+        returnObj = streamX.coreCopyAsDerivation('mergeVariantMeasureStreams')
 
     regions = _getRegionsFromStreams(returnObj, streamY)
     for (regionType, xRegionStartMeasure, xRegionEndMeasure,
@@ -644,7 +644,7 @@ def mergeVariantsEqualDuration(streams, variantNames, *, inPlace=False):
     if inPlace is True:
         returnObj = streams[0]
     else:
-        returnObj = copy.deepcopy(streams[0])
+        returnObj = streams[0].coreCopyAsDerivation('mergeVariantsEqualDuration')
 
     # Adds a None element at beginning (corresponding to default variant streams[0])
     variantNames.insert(0, None)
@@ -797,7 +797,7 @@ def mergePartAsOssia(mainPart, ossiaPart, ossiaName,
     if inPlace is True:
         returnObj = mainPart
     else:
-        returnObj = copy.deepcopy(mainPart)
+        returnObj = mainPart.coreCopyAsDerivation('mergePartAsOssia')
 
     if compareByMeasureNumber is True:
         for ossiaMeasure in ossiaPart.getElementsByClass('Measure'):
@@ -1044,7 +1044,7 @@ def refineVariant(s, sVariant, *, inPlace=False):
     else:
         sVariantIndex = s.variants.index(sVariant)
 
-        returnObject = copy.deepcopy(s)
+        returnObject = s.coreCopyAsDerivation('refineVariant')
         variantRegion = returnObject.variants(sVariantIndex)
 
 


Test patch:
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index 053ec4880..df95521d7 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -2249,6 +2249,10 @@ class Test(unittest.TestCase):
         # activeSite is Now sent to None
         self.assertEqual(n1.activeSite, None)
 
+        expected = "clef.TrebleClef is not a Music21Object; got <class 'str'>"
+        with self.assertRaisesRegex(TypeError, expected):
+            s.remove('clef.TrebleClef')
+
     def testRemoveByClass(self):
         s = Stream()
         s.repeatAppend(clef.BassClef(), 2)
@@ -8435,4 +8439,3 @@ class Test(unittest.TestCase):
 
 if __name__ == '__main__':
     music21.mainTest(Test, 'verbose')  # , runTest='test_makeBeams__paddingLeft_2_2')
-