Repository: cuthbertLab/music21
Pull request #852 (2021-02-13 15:13:37)

Add doctest and use .first() in getInstruments

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

Coverage increased (+0.001%) to 92.081% when pulling **74204910b2f40138b2865b81b5ec4dcdd50103a9 on jacobtylerwalls:instrument-first** into **a695860482070340ff51f3438d82be1ceb358e9e on cuthbertLab:master**.

mscuthbert: This is great -- I wonder if you'd mind setting the keywords to keyword-only?  It's a change that I'm implementing throughout and feel free to do it whenever there's no obvious order to keywords.

I am still thinking of eventually getting rid of these for a more general solution, but not in v7 in any case.

Patch:
diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py
index a472ef574..4e981fcc1 100644
--- a/music21/chord/__init__.py
+++ b/music21/chord/__init__.py
@@ -2218,6 +2218,16 @@ class Chord(note.NotRest):
         and `isDominantSeventh()` has intervalArray([0, 4, 7, 10])
 
         intervalArray can be any iterable.
+
+        Though it checks on intervalArray, it does make sure that it is a
+        seventh chord, not D--, D##, G, B-
+
+        >>> chord.Chord('C E G B-').isSeventhOfType((0, 4, 7, 10))
+        True
+        >>> chord.Chord('C E G B-').isSeventhOfType((0, 3, 7, 10))
+        False
+        >>> chord.Chord('D-- D## G B-').isSeventhOfType((0, 4, 7, 10))
+        False
         '''
         if not self.isSeventh():
             return False
@@ -3776,7 +3786,7 @@ class Chord(note.NotRest):
         if not match:
             raise ChordException(f'the given pitch is not in the Chord: {pitchTarget}')
 
-    def simplifyEnharmonics(self, *, inPlace=False):
+    def simplifyEnharmonics(self, *, inPlace=False, keyContext=None):
         '''
         Calls `pitch.simplifyMultipleEnharmonics` on the pitches of the chord.
 
@@ -3790,13 +3800,20 @@ class Chord(note.NotRest):
         >>> c.simplifyEnharmonics(inPlace=True)
         >>> c.pitches
         (<music21.pitch.Pitch C#>, <music21.pitch.Pitch E#>, <music21.pitch.Pitch G#>)
+
+        If `keyContext` is provided the enharmonics are simplified based on the supplied
+        Key or KeySignature.
+
+        >>> c.simplifyEnharmonics(inPlace=True, keyContext=key.Key('A-'))
+        >>> c.pitches
+        (<music21.pitch.Pitch D->, <music21.pitch.Pitch F>, <music21.pitch.Pitch A->)
         '''
         if inPlace:
             returnObj = self
         else:
             returnObj = copy.deepcopy(self)
 
-        pitches = pitch.simplifyMultipleEnharmonics(self.pitches)
+        pitches = pitch.simplifyMultipleEnharmonics(self.pitches, keyContext=keyContext)
         for i in range(len(pitches)):
             returnObj._notes[i].pitch = pitches[i]
 
diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py
index 362e2ad1f..a934c1f62 100644
--- a/music21/converter/__init__.py
+++ b/music21/converter/__init__.py
@@ -1171,15 +1171,16 @@ def parse(value: Union[bundles.MetadataEntry, bytes, str, pathlib.Path],
           and _osCanLoad(common.cleanpath(valueStr))):
         return parseFile(common.cleanpath(valueStr), number=number, format=m21Format,
                          forceSource=forceSource, **keywords)
-
     elif not isinstance(valueStr, bytes) and (valueStr.startswith('http://')
                                               or valueStr.startswith('https://')):
         # it's a url; may need to broaden these criteria
         return parseURL(value, number=number, format=m21Format,
                         forceSource=forceSource, **keywords)
-
     elif isinstance(value, pathlib.Path):
         raise FileNotFoundError(f'Cannot find file in {str(value)}')
+    elif (isinstance(value, str) and common.findFormatFile(value) is not None):
+        # assume mistyped file path
+        raise FileNotFoundError(f'Cannot find file in {str(value)}')
     else:
         return parseData(value, number=number, format=m21Format, **keywords)
 
@@ -1942,6 +1943,12 @@ class Test(unittest.TestCase):
 
         with self.assertRaises(FileNotFoundError):
             parse(fp)
+        with self.assertRaises(ConverterException):
+            # nonexistent path ending in incorrect extension
+            # no way to tell apart from data, so failure happens later
+            parse(str(fp))
+        with self.assertRaises(FileNotFoundError):
+            parse('nonexistent_path_ending_in_correct_extension.musicxml')
 
     def testParseURL(self):
         urlBase = 'http://kern.ccarh.org/cgi-bin/ksdata?l=users/craig/classical/'
diff --git a/music21/graph/axis.py b/music21/graph/axis.py
index 9a5b6777c..f541e7412 100644
--- a/music21/graph/axis.py
+++ b/music21/graph/axis.py
@@ -1289,7 +1289,7 @@ class CountingAxis(Axis):
         if not common.isIterable(countAxes):
             countAxes = (countAxes,)
 
-        axesIndices = tuple([self.axisDataMap[axisName] for axisName in countAxes])
+        axesIndices = tuple(self.axisDataMap[axisName] for axisName in countAxes)
         thisIndex = self.axisDataMap[self.axisName]
         selector = itemgetter(*axesIndices)
         relevantData = [selector(innerTuple) for innerTuple in client.data]
diff --git a/music21/musicxml/partStaffExporter.py b/music21/musicxml/partStaffExporter.py
index 1caabcee7..c19e57ed7 100644
--- a/music21/musicxml/partStaffExporter.py
+++ b/music21/musicxml/partStaffExporter.py
@@ -215,6 +215,7 @@ class PartStaffExporterMixin:
         staffGroups = self.stream.getElementsByClass('StaffGroup')
         joinableGroups: List[StaffGroup] = []
         # Joinable groups must consist of only PartStaffs with Measures
+        # and exist in self.stream
         for sg in staffGroups:
             if len(sg) <= 1:
                 continue
@@ -222,6 +223,8 @@ class PartStaffExporterMixin:
                 continue
             if not all(p.getElementsByClass('Measure') for p in sg):
                 continue
+            if not all(p in self.stream for p in sg):
+                continue
             joinableGroups.append(sg)
 
         # Deduplicate joinable groups (ex: bracket and brace enclose same PartStaffs)
@@ -904,6 +907,19 @@ class Test(unittest.TestCase):
         self.assertEqual({staff.text for staff in m2tag.findall('note/staff')}, {'2'})
         self.assertEqual({staff.text for staff in m3tag.findall('note/staff')}, {'1'})
 
+    def testJoinPartStaffsF(self):
+        '''
+        Flattening the score will leave StaffGroup spanners with parts no longer in the stream.
+        '''
+        from music21 import corpus
+        from music21 import musicxml
+        sch = corpus.parse('schoenberg/opus19', 2)
+
+        SX = musicxml.m21ToXml.ScoreExporter(sch.flat)
+        SX.scorePreliminaries()
+        SX.parsePartlikeScore()
+        # Previously, an exception was raised by getRootForPartStaff()
+        SX.joinPartStaffs()
 
 
 if __name__ == '__main__':
diff --git a/music21/prebase.py b/music21/prebase.py
index 93a46b979..33deef42a 100644
--- a/music21/prebase.py
+++ b/music21/prebase.py
@@ -150,7 +150,7 @@ class ProtoM21Object:
         try:
             return self._classTupleCacheDict[self.__class__]
         except KeyError:
-            classTuple = tuple([x.__name__ for x in self.__class__.mro()])
+            classTuple = tuple(x.__name__ for x in self.__class__.mro())
             self._classTupleCacheDict[self.__class__] = classTuple
             return classTuple
 
diff --git a/music21/roman.py b/music21/roman.py
index 54f606d0f..b67d08503 100644
--- a/music21/roman.py
+++ b/music21/roman.py
@@ -313,7 +313,8 @@ def postFigureFromChordAndKey(chordObj, keyObj=None):
         isMinorTriad = False
     else:
         isMajorTriad = chordObj.isMajorTriad()
-        isMinorTriad = chordObj.isMinorTriad()
+        # short-circuit this expensive call if we know it's not going to be true.
+        isMinorTriad = False if isMajorTriad else chordObj.isMinorTriad()
         chordObjIsStandardTriad = (
             isMajorTriad
             or isMinorTriad
@@ -469,6 +470,13 @@ def identifyAsTonicOrDominant(
     >>> roman.identifyAsTonicOrDominant(['B3', 'G4'], key.Key('g'))
     'i6'
 
+    >>> roman.identifyAsTonicOrDominant(['C3', 'B-4'], key.Key('f'))
+    'V7'
+
+    Notice that this -- with B-natural is also identified as V7 because
+    it is returning the roman numeral root and the inversion name, not yet
+    checking for correctness.
+
     >>> roman.identifyAsTonicOrDominant(['C3', 'B4'], key.Key('f'))
     'V7'
 
@@ -505,11 +513,11 @@ def identifyAsTonicOrDominant(
 
         oneMatches = len(set(onePitchNameList) & set(pitchNameList))
         fiveMatches = len(set(fivePitchNameList) & set(pitchNameList))
-        if oneMatches > fiveMatches and oneMatches > 0:
+        if oneMatches > fiveMatches:
             oneChordIdentified = True
-        elif oneMatches < fiveMatches and fiveMatches > 0:
+        elif oneMatches < fiveMatches:
             fiveChordIdentified = True
-        else:
+        else:  # both oneMatches and fiveMatches == 0
             return False
 
     if oneChordIdentified:
@@ -633,9 +641,11 @@ def correctRNAlterationForMinor(figureTuple, keyObj):
     return FigureTuple(figureTuple.aboveBass, alter, rootAlterationString)
 
 
-def romanNumeralFromChord(chordObj,
-                          keyObj: Union[key.Key, str] = None,
-                          preferSecondaryDominants=False):
+def romanNumeralFromChord(
+    chordObj,
+    keyObj: Union[key.Key, str] = None,
+    preferSecondaryDominants=False
+):
     # noinspection PyShadowingNames
     '''
     Takes a chord object and returns an appropriate chord name.  If keyObj is
@@ -770,6 +780,31 @@ def romanNumeralFromChord(chordObj,
     ...     )
     <music21.roman.RomanNumeral #io6b3 in C major>
 
+
+    Changed in v7 -- i7 is given for a tonic or subdominant minor-seventh chord in major:
+
+    >>> roman.romanNumeralFromChord(
+    ...     chord.Chord('C4 E-4 G4 B-4'),
+    ...     key.Key('C'))
+    <music21.roman.RomanNumeral i7 in C major>
+
+    >>> roman.romanNumeralFromChord(
+    ...     chord.Chord('E-4 G4 B-4 C5'),
+    ...     key.Key('G'))
+    <music21.roman.RomanNumeral iv65 in G major>
+
+    minor-Major chords are written with a [#7] modifier afterwards:
+
+    >>> roman.romanNumeralFromChord(
+    ...     chord.Chord('C4 E-4 G4 B4'),
+    ...     key.Key('C'))
+    <music21.roman.RomanNumeral i7[#7] in C major>
+    >>> roman.romanNumeralFromChord(
+    ...     chord.Chord('E-4 G4 B4 C5'),
+    ...     key.Key('C'))
+    <music21.roman.RomanNumeral i65[#7] in C major>
+
+
     Former bugs that are now fixed:
 
     >>> romanNumeral11 = roman.romanNumeralFromChord(
@@ -832,6 +867,22 @@ def romanNumeralFromChord(chordObj,
         'i64b3': 'Sw43',
         'io6b5b3': 'Ger65',
     }
+    minorSeventhSubs = {
+        'b75b3': '7',
+        '6b5': '65',
+        'b64b3': '43',
+        '6b42': '42',
+    }
+    minorMajorSeventhSubs = {
+        '75b3': '7[#7]',  # major key root
+        '65': '65[#7]',  # major key 1st inversion
+        'b643': '43[#7]',  # major key second inversion
+        '6b42': '42[#7]',  # major key form of 3rd inversion mM7...
+        '#753': '#7',  # root position in minor key
+        '6#53': '65[#7]',  # minor key 1st inversion
+        '64#3': '43[#7]',  # minor key 2nd inversion
+        '42': '42[#7]',  # minor key form of 3rd inversion mM7...
+    }
 
     noKeyGiven = (keyObj is None)
 
@@ -848,12 +899,12 @@ def romanNumeralFromChord(chordObj,
     else:
         isMajorThird = False
 
-    if isMajorThird:
-        rootKeyObj = _getKeyFromCache(root.name.upper())
-    else:
-        rootKeyObj = _getKeyFromCache(root.name.lower())
 
     if keyObj is None:
+        if isMajorThird:
+            rootKeyObj = _getKeyFromCache(root.name.upper())
+        else:
+            rootKeyObj = _getKeyFromCache(root.name.lower())
         keyObj = rootKeyObj
     elif isinstance(keyObj, str):
         keyObj = key.Key(keyObj)
@@ -889,7 +940,17 @@ def romanNumeralFromChord(chordObj,
 
     rnString = ft.prefix + stepRoman + inversionString
 
-    if not noKeyGiven and rnString in aug6subs and chordObj.isAugmentedSixth():
+    if (not isMajorThird
+            and inversionString in minorSeventhSubs
+            # only do expensive call in case it might be possible...
+            and chordObj.isSeventhOfType((0, 3, 7, 10))):
+        rnString = ft.prefix + stepRoman + minorSeventhSubs[inversionString]
+    elif (not isMajorThird
+              and inversionString in minorMajorSeventhSubs
+              and chordObj.isSeventhOfType((0, 3, 7, 11))):
+        rnString = ft.prefix + stepRoman + minorMajorSeventhSubs[inversionString]
+
+    elif not noKeyGiven and rnString in aug6subs and chordObj.isAugmentedSixth():
         rnString = aug6subs[rnString]
     elif noKeyGiven and rnString in aug6NoKeyObjectSubs and chordObj.isAugmentedSixth():
         rnString = aug6NoKeyObjectSubs[rnString]
@@ -1068,6 +1129,8 @@ class RomanNumeral(harmony.Harmony):
     >>> neapolitan2.scaleDegree
     2
 
+    Here's a dominant seventh chord in minor:
+
     >>> em = key.Key('e')
     >>> dominantV = roman.RomanNumeral('V7', em)
     >>> [str(p) for p in dominantV.pitches]
@@ -1077,6 +1140,13 @@ class RomanNumeral(harmony.Harmony):
     >>> [str(p) for p in minorV.pitches]
     ['F#4', 'A4', 'B4', 'D5']
 
+    (We will do this `str(p) for p in...` thing enough that let's make a helper function:
+
+    >>> def cp(rn_in):  # cp = chord pitches
+    ...     return [str(p) for p in rn_in.pitches]
+    >>> cp(minorV)
+    ['F#4', 'A4', 'B4', 'D5']
+
 
     In minor -- VII and VI are assumed to refer to the flattened scale degree.
     vii, viio, viio7, viiø7 and vi, vio, vio7, viø7 refer to the sharpened scale
@@ -1088,19 +1158,19 @@ class RomanNumeral(harmony.Harmony):
     The lowered (natural minor) is the assumed basic chord.
 
     >>> majorFlatSeven = roman.RomanNumeral('VII', em)
-    >>> [str(p) for p in majorFlatSeven.pitches]
+    >>> cp(majorFlatSeven)
     ['D5', 'F#5', 'A5']
 
     >>> minorSharpSeven = roman.RomanNumeral('vii', em)
-    >>> [str(p) for p in minorSharpSeven.pitches]
+    >>> cp(minorSharpSeven)
     ['D#5', 'F#5', 'A#5']
 
     >>> majorFlatSix = roman.RomanNumeral('VI', em)
-    >>> [str(p) for p in majorFlatSix.pitches]
+    >>> cp(majorFlatSix)
     ['C5', 'E5', 'G5']
 
     >>> minorSharpSix = roman.RomanNumeral('vi', em)
-    >>> [str(p) for p in minorSharpSix.pitches]
+    >>> cp(minorSharpSix)
     ['C#5', 'E5', 'G#5']
 
 
@@ -1108,7 +1178,7 @@ class RomanNumeral(harmony.Harmony):
     a member of :class:`music21.roman.Minor67Default`:
 
     >>> majorSharpSeven = roman.RomanNumeral('VII', em, seventhMinor=roman.Minor67Default.SHARP)
-    >>> [str(p) for p in majorSharpSeven.pitches]
+    >>> cp(majorSharpSeven)
     ['D#5', 'F##5', 'A#5']
 
     For instance, if you prefer a harmonic minor context where VI (or vi) always refers
@@ -1116,19 +1186,19 @@ class RomanNumeral(harmony.Harmony):
     `sixthMinor=roman.Minor67Default.FLAT` and `seventhMinor=roman.Minor67Default.SHARP`
 
     >>> dimHarmonicSeven = roman.RomanNumeral('viio', em, seventhMinor=roman.Minor67Default.SHARP)
-    >>> [str(p) for p in dimHarmonicSeven.pitches]
+    >>> cp(dimHarmonicSeven)
     ['D#5', 'F#5', 'A5']
 
     >>> majHarmonicSeven = roman.RomanNumeral('bVII', em, seventhMinor=roman.Minor67Default.SHARP)
-    >>> [str(p) for p in majHarmonicSeven.pitches]
+    >>> cp(majHarmonicSeven)
     ['D5', 'F#5', 'A5']
 
 
     >>> majHarmonicSix = roman.RomanNumeral('VI', em, sixthMinor=roman.Minor67Default.FLAT)
-    >>> [str(p) for p in majHarmonicSix.pitches]
+    >>> cp(majHarmonicSix)
     ['C5', 'E5', 'G5']
     >>> minHarmonicSix = roman.RomanNumeral('#vi', em, sixthMinor=roman.Minor67Default.FLAT)
-    >>> [str(p) for p in minHarmonicSix.pitches]
+    >>> cp(minHarmonicSix)
     ['C#5', 'E5', 'G#5']
 
 
@@ -1141,17 +1211,17 @@ class RomanNumeral(harmony.Harmony):
     Either of these is the same way of getting a minor iii in a minor key:
 
     >>> minoriii = roman.RomanNumeral('iii', em, caseMatters=True)
-    >>> [str(p) for p in minoriii.pitches]
+    >>> cp(minoriii)
     ['G4', 'B-4', 'D5']
 
     >>> minoriiiB = roman.RomanNumeral('IIIb', em, caseMatters=False)
-    >>> [str(p) for p in minoriiiB.pitches]
+    >>> cp(minoriiiB)
     ['G4', 'B-4', 'D5']
 
     `caseMatters=False` will prevent `sixthMinor` or `seventhMinor` from having effect.
     >>> vii = roman.RomanNumeral('viio', 'a', caseMatters=False,
     ...                           seventhMinor=roman.Minor67Default.QUALITY)
-    >>> [str(p) for p in vii.pitches]
+    >>> cp(vii)
     ['G5', 'B-5', 'D-6']
 
     Can also take a scale object, here we build a first-inversion chord
@@ -1162,7 +1232,7 @@ class RomanNumeral(harmony.Harmony):
     >>> sharp3.scaleDegreeWithAlteration
     (3, <music21.pitch.Accidental sharp>)
 
-    >>> [str(p) for p in sharp3.pitches]
+    >>> cp(sharp3)
     ['A#4', 'C#5', 'F#5']
 
     >>> sharp3.figure
@@ -1171,12 +1241,12 @@ class RomanNumeral(harmony.Harmony):
     Figures can be changed and pitches will change.
 
     >>> sharp3.figure = 'V'
-    >>> [str(p) for p in sharp3.pitches]
+    >>> cp(sharp3)
     ['A-4', 'C5', 'E-5']
 
     >>> leadingToneSeventh = roman.RomanNumeral(
     ...     'viio', scale.MajorScale('F'))
-    >>> [str(p) for p in leadingToneSeventh.pitches]
+    >>> cp(leadingToneSeventh)
     ['E5', 'G5', 'B-5']
 
     A little modal mixture:
@@ -1218,7 +1288,7 @@ class RomanNumeral(harmony.Harmony):
 
     >>> alteredChordHalfDim3rdInv = roman.RomanNumeral(
     ...     'biiø42', scale.MajorScale('F'))
-    >>> [str(p) for p in alteredChordHalfDim3rdInv.pitches]
+    >>> cp(alteredChordHalfDim3rdInv)
     ['F-4', 'G-4', 'B--4', 'D--5']
 
     >>> alteredChordHalfDim3rdInv.intervalVector
@@ -1248,7 +1318,7 @@ class RomanNumeral(harmony.Harmony):
     root. Music21 allows that:
 
     >>> fiveOhNine = roman.RomanNumeral('V9[no1]', key.Key('g'))
-    >>> [str(p) for p in fiveOhNine.pitches]
+    >>> cp(fiveOhNine)
     ['F#5', 'A5', 'C6', 'E-6']
 
     Putting [no] or [add] should never change the root
@@ -1268,7 +1338,7 @@ class RomanNumeral(harmony.Harmony):
     Putting it all together:
 
     >>> weirdChord = roman.RomanNumeral('V65[no5][add#6][b3]', key.Key('C'))
-    >>> [str(p) for p in weirdChord.pitches]
+    >>> cp(weirdChord)
     ['B-4', 'E#5', 'F5', 'G5']
     >>> weirdChord.root()
     <music21.pitch.Pitch G5>
@@ -1278,12 +1348,12 @@ class RomanNumeral(harmony.Harmony):
 
     >>> ots = scale.OctatonicScale('C2')
     >>> romanNumeral = roman.RomanNumeral('I9', ots, caseMatters=False)
-    >>> [str(p) for p in romanNumeral.pitches]
+    >>> cp(romanNumeral)
     ['C2', 'E-2', 'G-2', 'A2', 'C3']
 
     >>> romanNumeral2 = roman.RomanNumeral(
     ...     'V7#5b3', ots, caseMatters=False)
-    >>> [str(p) for p in romanNumeral2.pitches]
+    >>> cp(romanNumeral2)
     ['G-2', 'A-2', 'C#3', 'E-3']
 
     >>> romanNumeral = roman.RomanNumeral('v64/V', key.Key('e'))
@@ -1293,7 +1363,7 @@ class RomanNumeral(harmony.Harmony):
     >>> romanNumeral.figure
     'v64/V'
 
-    >>> [str(p) for p in romanNumeral.pitches]
+    >>> cp(romanNumeral)
     ['C#5', 'F#5', 'A5']
 
     >>> romanNumeral.secondaryRomanNumeral
@@ -1305,7 +1375,7 @@ class RomanNumeral(harmony.Harmony):
     >>> r.figure
     'bVIId7'
 
-    >>> [str(p) for p in r.pitches]
+    >>> cp(r)
     ['A-5', 'C6', 'E-6', 'G-6']
 
     >>> r = roman.RomanNumeral('VId7')
@@ -1313,11 +1383,11 @@ class RomanNumeral(harmony.Harmony):
     'VId7'
 
     >>> r.key = key.Key('B-')
-    >>> [str(p) for p in r.pitches]
+    >>> cp(r)
     ['G5', 'B5', 'D6', 'F6']
 
     >>> r2 = roman.RomanNumeral('V42/V7/vi', key.Key('C'))
-    >>> [str(p) for p in r2.pitches]
+    >>> cp(r2)
     ['A4', 'B4', 'D#5', 'F#5']
 
     >>> r2.secondaryRomanNumeral
@@ -1333,13 +1403,13 @@ class RomanNumeral(harmony.Harmony):
     >>> r = roman.RomanNumeral('Cad64', key.Key('C'))
     >>> r
     <music21.roman.RomanNumeral Cad64 in C major>
-    >>> [str(p) for p in r.pitches]
+    >>> cp(r)
     ['G4', 'C5', 'E5']
 
     >>> r = roman.RomanNumeral('Cad64', key.Key('c'))
     >>> r
     <music21.roman.RomanNumeral Cad64 in c minor>
-    >>> [str(p) for p in r.pitches]
+    >>> cp(r)
     ['G4', 'C5', 'E-5']
 
     Works also for secondary romans:
@@ -1347,10 +1417,69 @@ class RomanNumeral(harmony.Harmony):
     >>> r = roman.RomanNumeral('Cad64/V', key.Key('c'))
     >>> r
     <music21.roman.RomanNumeral Cad64/V in c minor>
-    >>> [str(p) for p in r.pitches]
+    >>> cp(r)
     ['D5', 'G5', 'B5']
 
 
+    In a major context, i7 and iv7 and their inversions are treated as minor-7th
+    chords:
+
+    >>> r = roman.RomanNumeral('i7', 'C')
+    >>> r
+    <music21.roman.RomanNumeral i7 in C major>
+    >>> cp(r)
+    ['C4', 'E-4', 'G4', 'B-4']
+
+    >>> r = roman.RomanNumeral('iv42', 'C')
+    >>> cp(r)
+    ['E-4', 'F4', 'A-4', 'C5']
+
+    For a minor-Major 7th chord in major, write it as i[add7] or i7[#7] or another inversion:
+
+    >>> minorMajor = roman.RomanNumeral('i[add7]', 'C')
+    >>> minorMajor
+    <music21.roman.RomanNumeral i[add7] in C major>
+    >>> cp(minorMajor)
+    ['C4', 'E-4', 'G4', 'B4']
+    >>> cp(roman.RomanNumeral('i7[#7]', 'C'))
+    ['C4', 'E-4', 'G4', 'B4']
+
+    Note that this is not the same as i#7, which gives a rather unusual chord in major.
+
+    >>> cp(roman.RomanNumeral('i#7', 'C'))
+    ['C4', 'E-4', 'G4', 'B#4']
+
+    In minor it's just fine, well, as fine:
+
+    >>> cp(roman.RomanNumeral('i#7', 'c'))
+    ['C4', 'E-4', 'G4', 'B4']
+
+
+    >>> cp(roman.RomanNumeral('i42[#7]', 'C'))
+    ['B4', 'C5', 'E-5', 'G5']
+
+    As noted above, Minor-Major 7th chords in minor have a different form in root position:
+
+    >>> cp(roman.RomanNumeral('i#7', 'c'))
+    ['C4', 'E-4', 'G4', 'B4']
+
+    (these are both the same)
+
+    >>> cp(roman.RomanNumeral('i#753', 'c'))
+    ['C4', 'E-4', 'G4', 'B4']
+    >>> cp(roman.RomanNumeral('i7[#7]', 'c'))
+    ['C4', 'E-4', 'G4', 'B4']
+
+
+    Other inversions are the same as with major keys:
+
+    >>> cp(roman.RomanNumeral('i65[#7]', 'c'))
+    ['E-4', 'G4', 'B4', 'C5']
+    >>> cp(roman.RomanNumeral('i43[#7]', 'c'))
+    ['G4', 'B4', 'C5', 'E-5']
+
+
+
     The RomanNumeral constructor accepts a keyword 'updatePitches' which is
     passed to harmony.Harmony. By default it
     is True, but can be set to False to initialize faster if pitches are not needed.
@@ -1366,41 +1495,41 @@ class RomanNumeral(harmony.Harmony):
 
     >>> dminor = key.Key('d')
     >>> rn = roman.RomanNumeral('iiø65', dminor)
-    >>> [str(p) for p in rn.pitches]
+    >>> cp(rn)
     ['G4', 'B-4', 'D5', 'E5']
 
     >>> rn.romanNumeral
     'ii'
 
     >>> rn3 = roman.RomanNumeral('III', dminor)
-    >>> [str(p) for p in rn3.pitches]
+    >>> cp(rn3)
     ['F4', 'A4', 'C5']
 
     Should be the same as above no matter when the key is set:
 
     >>> r = roman.RomanNumeral('VId7', key.Key('B-'))
-    >>> [str(p) for p in r.pitches]
+    >>> cp(r)
     ['G5', 'B5', 'D6', 'F6']
 
     >>> r.key = key.Key('B-')
-    >>> [str(p) for p in r.pitches]
+    >>> cp(r)
     ['G5', 'B5', 'D6', 'F6']
 
     This was getting B-flat.
 
     >>> r = roman.RomanNumeral('VId7')
     >>> r.key = key.Key('B-')
-    >>> [str(p) for p in r.pitches]
+    >>> cp(r)
     ['G5', 'B5', 'D6', 'F6']
 
     >>> r = roman.RomanNumeral('vio', em)
-    >>> [str(p) for p in r.pitches]
+    >>> cp(r)
     ['C#5', 'E5', 'G5']
 
     We can omit an arbitrary number of steps:
 
     >>> r = roman.RomanNumeral('Vd7[no3no5no7]', key.Key('C'))
-    >>> [str(p) for p in r.pitches]
+    >>> cp(r)
     ['G4']
 
 
@@ -1961,14 +2090,13 @@ class RomanNumeral(harmony.Harmony):
             # impliedQualitySymbol = '+'
         elif workingFigure.endswith('d7'):
             # this one is different
+            # # TODO(msc): what about d65, etc.?
             workingFigure = workingFigure[:-2] + '7'
             impliedQuality = 'dominant-seventh'
             # impliedQualitySymbol = '(dom7)'
-        elif (self.caseMatters
-              and self.romanNumeralAlone.upper() == self.romanNumeralAlone):
+        elif self.caseMatters and self.romanNumeralAlone.upper() == self.romanNumeralAlone:
             impliedQuality = 'major'
-        elif (self.caseMatters
-              and self.romanNumeralAlone.lower() == self.romanNumeralAlone):
+        elif self.caseMatters and self.romanNumeralAlone.lower() == self.romanNumeralAlone:
             impliedQuality = 'minor'
         self.impliedQuality = impliedQuality
         return workingFigure
@@ -2017,6 +2145,13 @@ class RomanNumeral(harmony.Harmony):
         ()
         >>> r._findSemitoneSizeForQuality('diminished')
         (3, 6, 9)
+
+        OMIT_FROM_DOCS
+
+        This one is not currently used.
+
+        >>> r._findSemitoneSizeForQuality('minor-seventh')
+        (3, 7, 10)
         '''
         if impliedQuality == 'major':
             correctSemitones = (4, 7)
@@ -2028,6 +2163,8 @@ class RomanNumeral(harmony.Harmony):
             correctSemitones = (3, 6, 10)
         elif impliedQuality == 'augmented':
             correctSemitones = (4, 8)
+        elif impliedQuality == 'minor-seventh':
+            correctSemitones = (3, 7, 10)
         elif impliedQuality == 'dominant-seventh':
             correctSemitones = (4, 7, 10)
         else:
@@ -2066,50 +2203,65 @@ class RomanNumeral(harmony.Harmony):
         >>> ' '.join([p.name for p in r.pitches])
         'C E- G- B--'
         '''
-        correctSemitones = self._findSemitoneSizeForQuality(impliedQuality)
-        chordStepsToExamine = (3, 5, 7)
-        # newPitches = []
-        for i in range(len(correctSemitones)):  # 3,5,7
-            thisChordStep = chordStepsToExamine[i]
-            skipThisChordStep = False
+        def correctFaultyPitch(faultyPitch, inner_correctedSemis):
+            if inner_correctedSemis >= 6:
+                inner_correctedSemis = -1 * (12 - inner_correctedSemis)
+            elif inner_correctedSemis <= -6:
+                inner_correctedSemis += 12
+
+            if faultyPitch.accidental is None:
+                faultyPitch.accidental = pitch.Accidental(inner_correctedSemis)
+            else:
+                acc = faultyPitch.accidental
+                inner_correctedSemis += acc.alter
+                if inner_correctedSemis >= 6:
+                    inner_correctedSemis = -1 * (12 - inner_correctedSemis)
+                elif inner_correctedSemis <= -6:
+                    inner_correctedSemis += 12
+
+                acc.set(inner_correctedSemis)
+
+        def shouldSkipThisChordStep(chordStep) -> bool:
+            '''
+            Skip adjusting chordSteps with explicit accidentals.
+
+            For a figure like V7b5, make sure not to correct the b5 back,
+            even though the implied quality requires a Perfect 5th.
+            '''
             for figure in self.figuresNotationObj.figures:
-                if (figure.number == thisChordStep
+                if (figure.number == chordStep
                         and figure.modifier.accidental is not None
                         and figure.modifier.accidental.alter != 0):
-                    # for a figure like V7b5, make sure not to correct the b5 back,
-                    # even though the implied quality requires a Perfect 5th.
-                    skipThisChordStep = True
+                    return True
+            return False
+
+
+        correctSemitones = self._findSemitoneSizeForQuality(impliedQuality)
+        chordStepsToExamine = (3, 5, 7)
+        # newPitches = []
 
-            if skipThisChordStep:
+        for i in range(len(correctSemitones)):  # 3, 5, possibly 7
+            thisChordStep = chordStepsToExamine[i]
+            if shouldSkipThisChordStep(thisChordStep):
                 continue
             thisCorrect = correctSemitones[i]
             thisSemis = self.semitonesFromChordStep(thisChordStep)
-            if thisSemis is None:
+            if thisSemis is None:  # no chord step
                 continue
-            if thisSemis == thisCorrect:
+            if thisSemis == thisCorrect:  # nothing to do
                 continue
 
             correctedSemis = thisCorrect - thisSemis
-            if correctedSemis >= 6:
-                correctedSemis = -1 * (12 - correctedSemis)
-            elif correctedSemis <= -6:
-                correctedSemis += 12
-
-            faultyPitch = self.getChordStep(thisChordStep)
-            if faultyPitch is None:  # pragma: no cover
-                raise RomanException(
-                    'this is very odd... should have been caught in semitonesFromChordStep')
-            if faultyPitch.accidental is None:
-                faultyPitch.accidental = pitch.Accidental(correctedSemis)
-            else:
-                acc = faultyPitch.accidental
-                correctedSemis += acc.alter
-                if correctedSemis >= 6:
-                    correctedSemis = -1 * (12 - correctedSemis)
-                elif correctedSemis <= -6:
-                    correctedSemis += 12
+            correctFaultyPitch(self.getChordStep(thisChordStep), correctedSemis)
+
+        if len(correctSemitones) == 2 and len(self.figuresNotationObj.figures) >= 3:
+            # special cases for chords whose 7th does not necessarily match the scale.
+            if self.impliedQuality == 'minor' and self.semitonesFromChordStep(7) == 11:
+                # i7 or iv7 chord or their inversions, in a major context.
+                # check first that this isn't on purpose...
+                if not shouldSkipThisChordStep(7):
+                    correctFaultyPitch(self.seventh, -1)
 
-                acc.set(correctedSemis)
 
     def _correctForSecondaryRomanNumeral(self, useScale, figure=None):
         '''
@@ -3643,6 +3795,74 @@ class Test(unittest.TestCase):
             # True, minor key:
             self.assertTrue(RomanNumeral(fig, 'a').isMixture())
 
+    def testMinorTonic7InMajor(self):
+        rn = RomanNumeral('i7', 'C')
+        pitchStrings = [p.name for p in rn.pitches]
+        self.assertEqual(pitchStrings, ['C', 'E-', 'G', 'B-'])
+        for k in (key.Key('C'), key.Key('c')):
+            ch1 = chord.Chord('C4 E-4 G4 B-4')
+            rn2 = romanNumeralFromChord(ch1, k)
+            self.assertEqual(rn2.figure, 'i7')
+            ch = chord.Chord('E-4 G4 B-4 C5')
+            rn = romanNumeralFromChord(ch, k)
+            self.assertEqual(rn.figure, 'i65')
+
+        for k in (key.Key('G'), key.Key('g')):
+            ch = chord.Chord('G4 B-4 C5 E-5')
+            rn = romanNumeralFromChord(ch, k)
+            self.assertEqual(rn.figure, 'iv43')
+            ch = chord.Chord('B-4 C5 E-5 G5')
+            rn = romanNumeralFromChord(ch, k)
+            self.assertEqual(rn.figure, 'iv42')
+
+    def testMinorMajor7InMajor(self):
+        def new_fig_equals_old_figure(rn_in, k='C'):
+            p_old = [p.name for p in rn_in.pitches]
+            rn_new = RomanNumeral(rn_in.figure, k)
+            p_new = [p.name for p in rn_new.pitches]
+            # order matters, octave does not
+            self.assertEqual(p_old, p_new, f'{p_old} not equal {p_new} for {rn_in}')
+
+        rn = RomanNumeral('i7[#7]', 'C')
+        pitchStrings = [p.name for p in rn.pitches]
+        self.assertEqual(pitchStrings, ['C', 'E-', 'G', 'B'])
+        ch1 = chord.Chord('C4 E-4 G4 B4')
+        rn1 = romanNumeralFromChord(ch1, 'C')
+        self.assertEqual(rn1.figure, 'i7[#7]')
+        new_fig_equals_old_figure(rn1)
+        ch2 = chord.Chord('E-4 G4 B4 C5')
+        rn2 = romanNumeralFromChord(ch2, 'C')
+        self.assertEqual(rn2.figure, 'i65[#7]')
+        new_fig_equals_old_figure(rn2)
+
+        ch3 = chord.Chord('G4 B4 C5 E-5')
+        rn3 = romanNumeralFromChord(ch3, 'G')
+        self.assertEqual(rn3.figure, 'iv43[#7]')
+        new_fig_equals_old_figure(rn3, 'G')
+        ch4 = chord.Chord('B4 C5 E-5 G5')
+        rn4 = romanNumeralFromChord(ch4, 'G')
+        self.assertEqual(rn4.figure, 'iv42[#7]')
+        new_fig_equals_old_figure(rn4, 'G')
+
+        # in minor these are more normal #7:
+        rn1 = romanNumeralFromChord(ch1, 'c')
+        self.assertEqual(rn1.figure, 'i#7')
+        new_fig_equals_old_figure(rn1, 'c')
+        rn2 = romanNumeralFromChord(ch2, 'c')
+        self.assertEqual(rn2.figure, 'i65[#7]')
+        new_fig_equals_old_figure(rn2, 'c')
+
+        ch3 = chord.Chord('G4 B4 C5 E-5')
+        rn3 = romanNumeralFromChord(ch3, 'g')
+        self.assertEqual(rn3.figure, 'iv43[#7]')
+        new_fig_equals_old_figure(rn3, 'g')
+        # except third-inversion...
+        ch4 = chord.Chord('B4 C5 E-5 G5')
+        rn4 = romanNumeralFromChord(ch4, 'g')
+        self.assertEqual(rn4.figure, 'iv42[#7]')
+        new_fig_equals_old_figure(rn4, 'g')
+
+
 
 class TestExternal(unittest.TestCase):  # pragma: no cover
 
diff --git a/music21/romanText/clercqTemperley.py b/music21/romanText/clercqTemperley.py
index f5aef3b03..aba19495c 100644
--- a/music21/romanText/clercqTemperley.py
+++ b/music21/romanText/clercqTemperley.py
@@ -766,7 +766,7 @@ class CTRule(prebase.ProtoM21Object):
         # third pass, make empty content duplicate previous content.
         for content, sep, numReps in measureGroups2:
             contentSplit = content.split()
-            if sep == '|' and all([y.startswith('[') or y == '' for y in contentSplit]):
+            if sep == '|' and all(y.startswith('[') or y == '' for y in contentSplit):
                 content = ' '.join(contentSplit)
                 if content:
                     content += ' '
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 1fd45561c..44bdd4493 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -2713,6 +2713,105 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         replaceDerived()
 
+    def splitAtDurations(self, *, recurse=False) -> base._SplitTuple:
+        '''
+        Overrides base method :meth:`~music21.base.Music21Object.splitAtDurations`
+        so that once each element in the stream having a complex duration is split
+        into similar, shorter elements representing each duration component,
+        the original element is actually replaced in the stream where it was found
+        with those new elements.
+
+        Returns a 1-tuple containing itself, for consistency with the superclass method.
+
+        >>> s = stream.Stream()
+        >>> s.insert(note.Note(quarterLength=5.0))
+        >>> post = s.splitAtDurations()
+        >>> post
+        (<music21.stream.Stream 0x10955ceb0>,)
+        >>> [n.duration for n in s]
+        [<music21.duration.Duration 4.0>, <music21.duration.Duration 1.0>]
+
+        Unless `recurse=True`, notes in substreams will not be found.
+
+        >>> s2 = stream.Score()
+        >>> p = stream.Part([note.Note(quarterLength=5)])
+        >>> s2.append(p)
+        >>> s2.splitAtDurations()
+        (<music21.stream.Score 0x10d12f100>,)
+        >>> [n.duration for n in s2.recurse().notes]
+        [<music21.duration.Duration 5.0>]
+        >>> s2.splitAtDurations(recurse=True)
+        (<music21.stream.Score 0x10d12f100>,)
+        >>> [n.duration for n in s2.recurse().notes]
+        [<music21.duration.Duration 4.0>, <music21.duration.Duration 1.0>]
+
+        `recurse=True` should not be necessary to find elements in streams
+        without substreams, such as a loose Voice:
+
+        >>> v = stream.Voice([note.Note(quarterLength=5.5)])
+        >>> v.splitAtDurations()
+        (<music21.stream.Voice 0x106020430>,)
+        >>> [n.duration for n in v.notes]
+        [<music21.duration.Duration 4.0>, <music21.duration.Duration 1.5>]
+
+        But a Voice in a Measure (most common) will not be found without `recurse`:
+
+        >>> m = stream.Measure()
+        >>> v2 = stream.Voice([note.Note(quarterLength=5.25)])
+        >>> m.insert(v2)
+        >>> m.splitAtDurations()
+        (<music21.stream.Measure 0 offset=0.0>,)
+        >>> [n.duration for n in m.recurse().notes]
+        [<music21.duration.Duration 5.25>]
+
+        For any spanner containing the element being removed, the first or last of the
+        replacing components replaces the removed element
+        (according to whether it was first or last in the spanner.)
+
+        >>> s3 = stream.Stream()
+        >>> n1 = note.Note(quarterLength=5)
+        >>> n2 = note.Note(quarterLength=5)
+        >>> s3.append([n1, n2])
+        >>> s3.insert(0, spanner.Slur([n1, n2]))
+        >>> post = s3.splitAtDurations()
+        >>> s3.spanners.first().getFirst() is n1
+        False
+        >>> s3.spanners.first().getFirst().duration
+        <music21.duration.Duration 4.0>
+        >>> s3.spanners.first().getLast().duration
+        <music21.duration.Duration 1.0>
+        '''
+
+        def processContainer(container: Stream):
+            for complexObj in container.getElementsNotOfClass(['Stream', 'Variant', 'Spanner']):
+                if complexObj.duration.type != 'complex':
+                    continue
+                insertPoint = complexObj.offset
+                objList = complexObj.splitAtDurations()
+
+                container.replace(complexObj, objList[0])
+                insertPoint += objList[0].quarterLength
+
+                for subsequent in objList[1:]:
+                    container.insert(insertPoint, subsequent)
+                    insertPoint += subsequent.quarterLength
+
+                # Replace elements in spanners
+                for sp in complexObj.getSpannerSites():
+                    if sp.getFirst() is complexObj:
+                        sp.replaceSpannedElement(complexObj, objList[0])
+                    if sp.getLast() is complexObj:
+                        sp.replaceSpannedElement(complexObj, objList[-1])
+
+        # Handle "loose" objects in self (usually just Measure or Voice)
+        processContainer(self)
+        # Handle inner streams
+        if recurse:
+            for innerStream in self.recurse(includeSelf=False, streamsOnly=True):
+                processContainer(innerStream)
+
+        return base._SplitTuple((self,))
+
     def splitAtQuarterLength(self,
                              quarterLength,
                              *,
@@ -4883,17 +4982,37 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         # environLocal.printDebug(['getTimeSignatures(): final result:', post[0]])
         return post
 
-    def getInstruments(self, searchActiveSite=True, returnDefault=True, recurse=False):
+    def getInstruments(self,
+                       *,
+                       searchActiveSite=True,
+                       returnDefault=True,
+                       recurse=False) -> 'music21.stream.Stream':
         '''
         Search this stream or activeSite streams for
-        :class:`~music21.instrument.Instrument` objects, otherwise
-        return a default Instrument
-        '''
-        # environLocal.printDebug(['searching for instrument, called from:',
-        #                        self])
-        # TODO: Rename: getInstruments, and return a Stream of instruments
-        # for cases when there is more than one instrument
+        :class:`~music21.instrument.Instrument` objects, and return a new stream
+        containing them. Otherwise, return a Stream containing a single default `Instrument`.
+
+        >>> p = stream.Part()
+        >>> m = stream.Measure([note.Note()])
+        >>> p.insert(0, m)
+        >>> instrumentStream = p.getInstruments(returnDefault=True)
+        >>> defaultInst = instrumentStream.first()
+        >>> defaultInst
+        <music21.instrument.Instrument ': '>
+
+        Insert the default instrument into the part:
+
+        >>> p.insert(0, defaultInst)
+
+        Searching the measure will find it only if the measure's active site is searched:
 
+        >>> search1 = p.measure(1).getInstruments(searchActiveSite=False, returnDefault=False)
+        >>> search1.first() is None
+        True
+        >>> search2 = p.measure(1).getInstruments(searchActiveSite=True, returnDefault=False)
+        >>> search2.first() is defaultInst
+        True
+        '''
         instObj = None
         if recurse:
             sIter = self.recurse()
@@ -4903,7 +5022,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         post = sIter.getElementsByClass('Instrument').stream()
         if post:
             # environLocal.printDebug(['found local instrument:', post[0]])
-            instObj = post[0]  # get first
+            instObj = post.first()
         else:
             if searchActiveSite:
                 # if isinstance(self.activeSite, Stream) and self.activeSite != self:
@@ -4930,9 +5049,12 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         # returns a Stream
         return post
 
-    def getInstrument(self, searchActiveSite=True, returnDefault=True):
+    def getInstrument(self,
+                      *,
+                      searchActiveSite=True,
+                      returnDefault=True) -> Optional['music21.instrument.Instrument']:
         '''
-        Return the first Instrument found in this Stream.
+        Return the first Instrument found in this Stream, or None.
 
         >>> s = stream.Score()
         >>> p1 = stream.Part()
@@ -4956,10 +5078,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         '''
         post = self.getInstruments(searchActiveSite=searchActiveSite,
                                    returnDefault=returnDefault)
-        if post:
-            return post[0]
-        else:
-            return None
+        return post.first()
 
     def getClefs(self, searchActiveSite=False, searchContext=True,
                  returnDefault=True):
@@ -6381,7 +6500,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         '''
         This method calls a sequence of Stream methods on this Stream to prepare
         notation, including creating voices for overlapped regions, Measures
-        if necessary, creating ties, beams, and accidentals.
+        if necessary, creating ties, beams, accidentals, and tuplet brackets.
 
         If `inPlace` is True, this is done in-place;
         if `inPlace` is False, this returns a modified deep copy.
@@ -6432,6 +6551,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         measureStream = returnStream.getElementsByClass('Measure').stream()
         # environLocal.printDebug(['Stream.makeNotation(): post makeMeasures,
         #   length', len(returnStream)])
+        if not measureStream:
+            raise StreamException(
+                f'no measures found in stream with {len(self)} elements')
 
         # for now, calling makeAccidentals once per measures
         # pitches from last measure are passed
@@ -6468,22 +6590,15 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                     tiePitchSet=tiePitchSet,
                     inPlace=True,
                     **srkCopy)
-        # environLocal.printDebug(['makeNotation(): meterStream:', meterStream, meterStream[0]])
-        try:
-            measureStream.makeTies(meterStream, inPlace=True)
-        except StreamException as e:
-            strE = str(e)
-            environLocal.warn('StreamException: makeTies: ' + strE)
+
+        measureStream.makeTies(meterStream, inPlace=True)
 
         # measureStream.makeBeams(inPlace=True)
         if not measureStream.streamStatus.beams:
             try:
                 measureStream.makeBeams(inPlace=True)
-            except (StreamException, meter.MeterException):
-                # this is a result of makeMeasures not getting everything
-                # note to measure allocation right
-                # environLocal.printDebug(['skipping makeBeams exception', StreamException])
-                pass
+            except meter.MeterException as me:
+                environLocal.warn(['skipping makeBeams exception', me])
 
         # note: this needs to be after makeBeams, as placing this before
         # makeBeams was causing the duration's tuplet to lose its type setting
@@ -6493,12 +6608,6 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             if m.streamStatus.haveTupletBracketsBeenMade() is False:
                 makeNotation.makeTupletBrackets(m, inPlace=True)
 
-        if not measureStream:
-            raise StreamException(
-                f'no measures found in stream with {len(self)} elements')
-        # environLocal.printDebug(['Stream.makeNotation(): created measures:',
-        #   len(measureStream)])
-
         return returnStream
 
     def extendDuration(self, objName, *, inPlace=False):
@@ -9180,29 +9289,42 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> s.parts[0].getElementsByClass('Measure').first().isWellFormedNotation()
         True
 
-        >>> s = stream.Score()
+        >>> s2 = stream.Score()
         >>> m = stream.Measure()
-        >>> s.append(m)
-        >>> s.isWellFormedNotation()
+        >>> s2.append(m)
+        >>> s2.isWellFormedNotation()
+        False
+
+        >>> o = stream.Opus([s])
+        >>> o.isWellFormedNotation()
+        True
+        >>> o2 = stream.Opus([s2])
+        >>> o2.isWellFormedNotation()
+        False
+
+        Only Measures and Voices are allowed to contain notes and rests directly:
+        >>> m.isWellFormedNotation()
+        True
+        >>> s2.append(note.Rest())
+        >>> s2.isWellFormedNotation()
         False
         '''
-        # if a measure, we assume we are well-formed
-        if 'Measure' in self.classes:
+        def allSubstreamsHaveMeasures(testStream):
+            return all(s.hasMeasures() for s in testStream.getElementsByClass('Stream'))
+
+        # if a measure or voice, we assume we are well-formed
+        if 'Measure' in self.classes or 'Voice' in self.classes:
             return True
+        # all other Stream classes are not well-formed if they have "loose" notes
+        elif self.getElementsByClass('GeneralNote'):
+            return False
         elif 'Part' in self.classes:
             if self.hasMeasures():
                 return True
+        elif 'Opus' in self.classes:
+            return all(allSubstreamsHaveMeasures(s) for s in self.scores)
         elif self.hasPartLikeStreams():
-            # higher constraint than has part-like streams: has sub-streams
-            # with Measures
-            match = 0
-            count = 0
-            for s in self.getElementsByClass('Stream'):
-                count += 1
-                if s.hasMeasures():
-                    match += 1
-            if match == count:
-                return True
+            return allSubstreamsHaveMeasures(self)
         # all other conditions are not well-formed notation
         return False
 
@@ -12356,13 +12478,7 @@ class Measure(Stream):
 
         # environLocal.printDebug(['have time signature', m.timeSignature])
         if m.streamStatus.beams is False:
-            try:
-                m.makeBeams(inPlace=True)
-            except StreamException:
-                # this is a result of makeMeasures not getting everything
-                # note to measure allocation right
-                pass
-                # environLocal.printDebug(['skipping makeBeams exception', StreamException])
+            m.makeBeams(inPlace=True)
         if m.streamStatus.haveTupletBracketsBeenMade() is False:
             makeNotation.makeTupletBrackets(m, inPlace=True)
 
@@ -12881,7 +12997,7 @@ class Part(Stream):
         alteredPitches=None,
         cautionaryPitchClass=True,
         cautionaryAll=False,
-        inPlace=True,
+        inPlace=False,
         overrideStatus=False,
         cautionaryNotImmediateRepeat=True,
         tiePitchSet=None,
@@ -12891,7 +13007,7 @@ class Part(Stream):
         provides the management of passing pitches from
         a past Measure to each new measure for processing.
 
-        TODO: by default inPlace should be False
+        Changed in v.7 -- `inPlace` defaults False
         '''
         if not inPlace:  # make a copy
             returnObj = self.coreCopyAsDerivation('makeAccidentals')
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index c9d12706b..da1a92f47 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -118,11 +118,8 @@ def makeBeams(
         if m.timeSignature is not None:
             lastTimeSignature = m.timeSignature
         if lastTimeSignature is None:
-            # environLocal.printDebug([
-            #    'makeBeams(): lastTimeSignature is None: cannot process'])
-            # TODO: Reduce to warning...
-            raise stream.StreamException(
-                'cannot process beams in a Measure without a time signature')
+            environLocal.printDebug('cannot process beams in a Measure without a time signature')
+            continue
         noteGroups = []
         if m.hasVoices():
             for v in m.voices:
@@ -613,6 +610,7 @@ def makeMeasures(
         if not match:
             if start == end == oMax:
                 post.storeAtEnd(e)
+                continue
             else:
                 raise stream.StreamException(
                     f'cannot place element {e} with start/end {start}/{end} within any measures')


Test patch:
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index 4817fff3c..4fb6ed11c 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -2044,7 +2044,7 @@ class Test(unittest.TestCase):
         obj = expressions.TextExpression('FREEZE')
         s.insert(3, obj)
         s.makeMeasures(inPlace=True)
-        self.assertEqual(len(s.measure(1).getElementsByClass('Expression')), 1)
+        self.assertEqual(len(s.flat.getElementsByClass('Expression')), 1)
 
     def testRemove(self):
         '''Test removing components from a Stream.
@@ -4775,7 +4775,7 @@ class Test(unittest.TestCase):
         post = s.sliceByQuarterLengths(0.125, inPlace=False)
         # post.show()
 
-        self.assertTrue(all([n.tie is not None for n in post.notesAndRests]))
+        self.assertTrue(all(n.tie is not None for n in post.notesAndRests))
 
         s = Stream()
         n1 = note.Note()