Repository: cuthbertLab/music21
Pull request #1049 (2021-06-30 22:23:54)

Support and recommend `.sorted()` instead of `.sorted`
As mentioned in v6.7 release notes.

coveralls: 
[![Coverage Status](https://coveralls.io/builds/41023093/badge)](https://coveralls.io/builds/41023093)

Coverage increased (+0.0002%) to 92.34% when pulling **1eb2c11d61287f3ddb0d7661392361419ca9c2de on jacobtylerwalls:sorted** into **2c21a9921b55ffa906c828ab91739592780f9c0b on cuthbertLab:master**.


Patch:
diff --git a/documentation/source/usersGuide/usersGuide_55_advancedMeter.ipynb b/documentation/source/usersGuide/usersGuide_55_advancedMeter.ipynb
index 360975f6f..514b8087a 100644
--- a/documentation/source/usersGuide/usersGuide_55_advancedMeter.ipynb
+++ b/documentation/source/usersGuide/usersGuide_55_advancedMeter.ipynb
@@ -1413,7 +1413,7 @@
     "                mark = articulations.Accent()\n",
     "            n.articulations.append(mark)\n",
     "            lastBeat = beat\n",
-    "        m = m.sorted\n",
+    "        m = m.sorted()\n",
     "        \n",
     "partBass.measures(0, 8).show() "
    ]
diff --git a/music21/instrument.py b/music21/instrument.py
index dd0e3b22f..1dfee6a9b 100644
--- a/music21/instrument.py
+++ b/music21/instrument.py
@@ -384,6 +384,24 @@ class Sampler(KeyboardInstrument):
         self.instrumentAbbreviation = 'Samp'
         self.midiProgram = 55
 
+
+class ElectricPiano(Piano):
+    '''
+
+    >>> p = instrument.ElectricPiano()
+    >>> p.instrumentName
+    'Electric Piano'
+    >>> p.midiProgram
+    2
+    '''
+    def __init__(self):
+        super().__init__()
+
+        self.instrumentName = 'Electric Piano'
+        self.instrumentAbbreviation = 'E.Pno'
+        self.midiProgram = 2
+
+
 # ------------------------------------------------------------------------------
 
 
@@ -1650,7 +1668,7 @@ class Vocalist(Instrument):
 
         self.instrumentName = 'Voice'
         self.instrumentAbbreviation = 'V'
-        self.midiProgram = 52
+        self.midiProgram = 53
 
 
 class Soprano(Vocalist):
@@ -1706,6 +1724,16 @@ class Bass(Vocalist):
         self.instrumentAbbreviation = 'B'
         self.instrumentSound = 'voice.bass'
 
+
+class Choir(Vocalist):
+    def __init__(self):
+        super().__init__()
+
+        self.instrumentName = 'Choir'
+        self.instrumentAbbreviation = 'Ch'
+        self.instrumentSound = 'voice.choir'
+        self.midiProgram = 52
+
 # -----------------------------------------------------
 
 
@@ -1881,10 +1909,10 @@ def deduplicate(s: Stream, inPlace: bool = False) -> Stream:
 MIDI_PROGRAM_TO_INSTRUMENT = {
     0: Piano,
     1: Piano,
-    2: Piano,
+    2: ElectricPiano,
     3: Piano,
-    4: Piano,
-    5: Piano,
+    4: ElectricPiano,
+    5: ElectricPiano,
     6: Harpsichord,
     7: Clavichord,
     8: Celesta,
@@ -1931,9 +1959,9 @@ MIDI_PROGRAM_TO_INSTRUMENT = {
     49: StringInstrument,  # TODO: instrumentSound
     50: StringInstrument,  # TODO: instrumentSound
     51: StringInstrument,  # TODO: instrumentSound
-    52: Vocalist,  # TODO: instrumentSound
-    53: Vocalist,   # TODO: instrumentSound
-    54: Vocalist,   # TODO: instrumentSound
+    52: Choir,  # TODO: instrumentSound
+    53: Vocalist,  # TODO: instrumentSound
+    54: Vocalist,  # TODO: instrumentSound
     55: Sampler,
     56: Trumpet,
     57: Trombone,
@@ -1955,7 +1983,7 @@ MIDI_PROGRAM_TO_INSTRUMENT = {
     73: Flute,
     74: Recorder,
     75: PanFlute,
-    # 76: Bottle
+    76: PanFlute,  # TODO 76: Bottle
     77: Shakuhachi,
     78: Whistle,
     79: Ocarina,
@@ -1991,7 +2019,7 @@ MIDI_PROGRAM_TO_INSTRUMENT = {
     109: Bagpipes,
     110: Violin,  # TODO: instrumentSound
     111: Shehnai,
-    # 112: Tinkle Bell
+    112: Glockenspiel,  # TODO 112: Tinkle Bell
     113: Agogo,
     114: SteelDrum,
     115: Woodblock,
@@ -2017,7 +2045,7 @@ def instrumentFromMidiProgram(number: int) -> Instrument:
     Lookups are performed against `instrument.MIDI_PROGRAM_TO_INSTRUMENT`.
 
     >>> instrument.instrumentFromMidiProgram(4)
-    <music21.instrument.Piano 'Piano'>
+    <music21.instrument.ElectricPiano 'Electric Piano'>
     >>> instrument.instrumentFromMidiProgram(21)
     <music21.instrument.Accordion 'Accordion'>
     >>> instrument.instrumentFromMidiProgram(500)
@@ -2339,6 +2367,18 @@ def fromString(instrumentString):
     >>> t11 = instrument.fromString('Cl. in B-flat')
     >>> t11.__class__ == t10.__class__
     True
+
+
+    Previously an exact instrument name was not always working:
+
+    >>> instrument.fromString('Flute')
+    <music21.instrument.Flute 'Flute'>
+
+    This common MIDI instrument was not previously working:
+
+    >>> instrument.fromString('Choir (Aahs)')
+    <music21.instrument.Choir 'Choir (Aahs)'>
+
     '''
     # pylint: disable=undefined-variable
     from music21.languageExcerpts import instrumentLookup
@@ -2351,9 +2391,14 @@ def fromString(instrumentString):
     bestInstClass = None
     bestInstrument = None
     bestName = None
+
     for substring in allCombinations:
+        substring = substring.lower()
         try:
-            englishName = instrumentLookup.allToBestName[substring.lower()]
+            if substring in instrumentLookup.bestNameToInstrumentClass:
+                englishName = substring
+            else:
+                englishName = instrumentLookup.allToBestName[substring]
             className = instrumentLookup.bestNameToInstrumentClass[englishName]
 
             # This would be unsafe...
@@ -2380,7 +2425,7 @@ def fromString(instrumentString):
             pass
     if bestInstClass is None:
         raise InstrumentException(
-            f'Could not match string with instrument: {instrumentString}')
+            f'Could not match string with instrument: {instrumentStringOrig}')
     if bestName not in instrumentLookup.transposition:
         return bestInstrument
 
diff --git a/music21/languageExcerpts/instrumentLookup.py b/music21/languageExcerpts/instrumentLookup.py
index caa83528a..fcd14c43f 100644
--- a/music21/languageExcerpts/instrumentLookup.py
+++ b/music21/languageExcerpts/instrumentLookup.py
@@ -45,6 +45,7 @@ abbreviationToBestName = {
     'cb': 'contrabass',
     'cel': 'celesta',
     'cga dr': 'conga drum',
+    'ch': 'choir',
     'cl': 'clarinet',
     'clst': 'celesta',
     'clv': 'clavichord',
@@ -173,6 +174,7 @@ bestNameToInstrumentClass = {
     'bongo drums': 'BongoDrums',
     'castanets': 'Castanets',
     'celesta': 'Celesta',
+    'choir': 'Choir',
     'clarinet': 'Clarinet',
     'clavichord': 'Clavichord',
     'conga drum': 'CongaDrum',
@@ -182,6 +184,7 @@ bestNameToInstrumentClass = {
     'dulcimer': 'Dulcimer',
     'electric bass': 'ElectricBass',
     'electric guitar': 'ElectricGuitar',
+    'electric piano': 'ElectricPiano',
     'electric organ': 'ElectricOrgan',
     'english horn': 'EnglishHorn',
     'finger cymbals': 'FingerCymbals',
@@ -261,6 +264,7 @@ englishToBestName = {
     'agogo': 'agogo',
     'alto': 'alto',
     'alto saxophone': 'alto saxophone',
+    'bagpipe': 'bagpipes',
     'bagpipes': 'bagpipes',
     'banjo': 'banjo',
     'baritone': 'baritone',
@@ -348,6 +352,7 @@ englishToBestName = {
     'snare drum': 'snare drum',
     'soprano': 'soprano',
     'soprano saxophone': 'soprano saxophone',
+    'steel drums': 'steel drum',
     'steel drum': 'steel drum',
     'steel pan': 'steel drum',
     'suspended cymbal': 'suspended cymbal',
@@ -1141,7 +1146,8 @@ transliteration = {
     'violin': 'viol\xedn',
     'xilofon': 'xilof\xf3n',
     'xilofono': 'xil\xf3fono',
-    'zamponas': 'zampo\xf1as'}
+    'zamponas': 'zampo\xf1as',
+}
 
 transposition = {
     'clarinet': {'a': 'm-3',
diff --git a/music21/metadata/__init__.py b/music21/metadata/__init__.py
index 11f8f45cd..105e999c7 100644
--- a/music21/metadata/__init__.py
+++ b/music21/metadata/__init__.py
@@ -1198,7 +1198,7 @@ class RichMetadata(Metadata):
 
         environLocal.printDebug(['RichMetadata: update(): start'])
 
-        flat = streamObj.flat.sorted
+        flat = streamObj.flat.sorted()
 
         self.numberOfParts = len(streamObj.parts)
         self.keySignatureFirst = None
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 65a7d3725..bf1e26fb1 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -4362,10 +4362,10 @@ class MeasureParser(XMLParserBase):
         m = self.stream
         if not textStripValid(mxVoice):
             useVoice = self.lastVoice
-            if useVoice is None:
-                environLocal.warn('Cannot put in an element with a missing voice tag when '
-                                  + 'no previous voice tag was given.  Assuming voice 1... '
-                                  )
+            if useVoice is None:  # pragma: no cover
+                environLocal.warn(
+                    'Cannot put in an element with a missing voice tag when '
+                    + 'no previous voice tag was given.  Assuming voice 1... ')
                 useVoice = 1
         else:
             useVoice = mxVoice.text.strip()
diff --git a/music21/note.py b/music21/note.py
index 5b99a62d4..ce21d3d96 100644
--- a/music21/note.py
+++ b/music21/note.py
@@ -584,7 +584,7 @@ class GeneralNote(base.Music21Object):
             return None
 
         allText = [ly.text for ly in self.lyrics]
-        return '\n'.join(allText)
+        return '\n'.join([t for t in allText if t is not None])
 
     def _setLyric(self, value: Union[str, Lyric, None]) -> None:
         self.lyrics = []
diff --git a/music21/stream/base.py b/music21/stream/base.py
index e452f7a80..dfac870f3 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -305,6 +305,21 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         self.coreElementsChanged()
 
+    def __call__(self):
+        '''
+        Temporary workaround to support both prior usage of `.sorted`
+        and new recommended usage of `.sorted()`.
+        During the period where `.sorted` is still supported, even calling `.sorted()`
+        (recommended) will retrieve the property `.sorted` and necessitate
+        this workaround.
+
+        Returns `self` without any changes.
+
+        TODO: manage and emit DeprecationWarnings in v.8
+        TODO: remove in v.9
+        '''
+        return self
+
     def _reprInternal(self):
         if self.id is not None:
             if self.id != id(self) and str(self.id) != str(id(self)):
@@ -7285,7 +7300,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         a Note at offset 0)
 
         if this Stream is not flat, then only the highest elements are sorted.  To sort all,
-        run myStream.flat.sorted
+        run myStream.flat.sorted()
 
         For instance, here is an unsorted Stream
 
@@ -7300,7 +7315,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         But a sorted version of the Stream puts the C first:
 
-        >>> s.sorted.show('text')
+        >>> s.sorted().show('text')
         {0.0} <music21.note.Note C>
         {1.0} <music21.note.Note D>
 
@@ -7324,7 +7339,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         ...    g += '%s: %s; ' % (myElement.offset, myElement.name)
         >>> g
         '0.0: C#; 2.0: C#; 4.0: C#; 1.0: D-; 3.0: D-; 5.0: D-; '
-        >>> y = s.sorted
+        >>> y = s.sorted()
         >>> y.isSorted
         True
         >>> g = ''
@@ -7341,7 +7356,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         ...    g += '%s: %s; ' % (myElement.offset, myElement.name)
         >>> g
         '0.0: C#; 1.0: D-; 2.0: C#; 3.0: D-; 4.0: C#; 5.0: D-; 2.0: E; '
-        >>> z = y.sorted
+        >>> z = y.sorted()
         >>> g = ''
         >>> for myElement in z:
         ...    g += '%s: %s; ' % (myElement.offset, myElement.name)
@@ -7528,7 +7543,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> g
         '0.0: C#; 2.0: C#; 4.0: C#; 1.0: D-; 3.0: D-; 5.0: D-; '
 
-        >>> y = s.sorted
+        >>> y = s.sorted()
         >>> y.isSorted
         True
 
@@ -10013,7 +10028,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         '''
         flatStream = self.flat
         if flatStream.isSorted is False:
-            flatStream = flatStream.sorted
+            flatStream = flatStream.sorted()
         # these may not be sorted
         durSpanSorted = self._getDurSpan(flatStream)
         # According to the above comment, the spans may not be sorted
@@ -10049,7 +10064,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         '''
         flatStream = self.flat
         if flatStream.isSorted is False:
-            flatStream = flatStream.sorted
+            flatStream = flatStream.sorted()
 
         if len(layeringMap) != len(flatStream):
             raise StreamException('layeringMap must be the same length as flatStream')
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index 894903136..f1a71b5e2 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -424,7 +424,7 @@ def makeMeasures(
         else:
             srcObj = s.flat
         if not srcObj.isSorted:
-            srcObj = srcObj.sorted
+            srcObj = srcObj.sorted()
         if not inPlace:
             srcObj = copy.deepcopy(srcObj)
         voiceCount = len(srcObj.voices)
@@ -682,7 +682,7 @@ def makeMeasures(
         if post.isSorted:
             postSorted = post
         else:
-            postSorted = post.sorted
+            postSorted = post.sorted()
 
         for e in postSorted:
             # may need to handle spanners; already have s as site
diff --git a/music21/voiceLeading.py b/music21/voiceLeading.py
index 633f8ec4c..abfea2540 100644
--- a/music21/voiceLeading.py
+++ b/music21/voiceLeading.py
@@ -110,8 +110,8 @@ class VoiceLeadingQuartet(base.Music21Object):
         self.v2n1 = v2n1
         self.v2n2 = v2n2
 
-        self.vIntervals = []  # vertical intervals (harmonic)
-        self.hIntervals = []  # horizontal intervals (melodic)
+        self.vIntervals: List[interval.Interval] = []  # vertical intervals (harmonic)
+        self.hIntervals: List[interval.Interval] = []  # horizontal intervals (melodic)
 
         self._key = None
         if analyticKey is not None:


Test patch:
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index a83dc587a..58eb22af6 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -307,7 +307,7 @@ class Test(unittest.TestCase):
         s.repeatInsert(note.Note('C#'), [0.0, 2.0, 4.0])
         s.repeatInsert(note.Note('D-'), [1.0, 3.0, 5.0])
         self.assertFalse(s.isSorted)
-        y = s.sorted
+        y = s.sorted()
         self.assertTrue(y.isSorted)
         g = ''
         for myElement in y:
@@ -400,8 +400,8 @@ class Test(unittest.TestCase):
         n1 = note.Note()
         st2.insert(10, n1)
         st1.insert(12, st2)
-        self.assertIs(st1.flat.sorted[0], n1)
-        self.assertEqual(st1.flat.sorted[0].offset, 22.0)
+        self.assertIs(st1.flat.sorted()[0], n1)
+        self.assertEqual(st1.flat.sorted()[0].offset, 22.0)
 
     def testStreamExceptionsOnAssert(self):
         with self.assertRaises(StreamException):
@@ -432,7 +432,7 @@ class Test(unittest.TestCase):
         # environLocal.printDebug(['pre flat of mid stream'])
         self.assertEqual(len(midStream.flat), 24)
 #        self.assertEqual(len(midStream.getOverlaps()), 0)
-        mfs = midStream.flat.sorted
+        mfs = midStream.flat.sorted()
         self.assertEqual(mfs[7].getOffsetBySite(mfs), 11.0)
 
         farStream = Stream()
@@ -473,7 +473,7 @@ class Test(unittest.TestCase):
 
         # get just offset times
         # elementsSorted returns offset, dur, element
-        fs_fs = farStream.flat.sorted
+        fs_fs = farStream.flat.sorted()
         offsets = [a.offset for a in fs_fs]  # safer is a.getOffsetBySite(fs_fs)
         offsetsBrief = offsets[:20]
         self.assertEqual(offsetsBrief,
@@ -4341,7 +4341,7 @@ class Test(unittest.TestCase):
 
         self.assertEqual([x.name for x in s], ['B', 'A'])
         # try getting sorted
-        sSorted = s.sorted
+        sSorted = s.sorted()
         # original unchanged
         self.assertEqual([x.name for x in s], ['B', 'A'])
         # new is changed