Repository: cuthbertLab/music21
Pull request #1029 (2021-05-23 17:41:22)

Let `asKey()` solve for mode from tonic and sharps
I built this for someone, and I'm offering it back upstream. It's what I had suggested in reviewing #781.

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

Coverage increased (+0.002%) to 92.287% when pulling **a8d7dc3fa1ed637836bbb1947028438cfcf95823 on jacobtylerwalls:solve-mode** into **e030bee3c401c45ab616949ad5c464f2d8eb8a9f on cuthbertLab:master**.


Patch:
diff --git a/music21/analysis/floatingKey.py b/music21/analysis/floatingKey.py
index b8970fffa..de5bae76d 100644
--- a/music21/analysis/floatingKey.py
+++ b/music21/analysis/floatingKey.py
@@ -86,7 +86,7 @@ class KeyAnalyzer:
 
         self.weightAlgorithm = divide
         if s.hasPartLikeStreams():
-            p = s.iter.parts.first()
+            p = s.iter().parts.first()
         else:
             p = s
         self.numMeasures = len(p.getElementsByClass('Measure'))  # could be wrong for endings, etc.
diff --git a/music21/analysis/reduction.py b/music21/analysis/reduction.py
index 96c44cca0..e4c44868d 100644
--- a/music21/analysis/reduction.py
+++ b/music21/analysis/reduction.py
@@ -585,7 +585,7 @@ class PartReduction:
                     # check for activity in any part in the part group
                     for p in partMeasures:  # iter of parts containing measures
                         # print(p, i, p[i], len(p[i].flat.notes))
-                        if p[i].iter.notes:
+                        if p[i].iter().notes:
                             active = True
                             break
                     # environLocal.printDebug([i, 'active', active])
diff --git a/music21/base.py b/music21/base.py
index 339869e78..48d367178 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -1988,8 +1988,8 @@ class Music21Object(prebase.ProtoM21Object):
         The `className` can be used to specify one or more classes to match.
 
         >>> s = corpus.parse('bwv66.6')
-        >>> m2 = s.parts[0].iter.getElementsByClass('Measure')[2]  # pickup measure
-        >>> m3 = s.parts[0].iter.getElementsByClass('Measure')[3]
+        >>> m2 = s.parts[0].getElementsByClass('Measure')[2]  # pickup measure
+        >>> m3 = s.parts[0].getElementsByClass('Measure')[3]
         >>> m3
         <music21.stream.Measure 3 offset=9.0>
         >>> m3prev = m3.previous()
@@ -2002,7 +2002,7 @@ class Music21Object(prebase.ProtoM21Object):
 
         We'll iterate backwards from the first note of the second measure of the Alto part.
 
-        >>> o = s.parts[1].iter.getElementsByClass('Measure')[2][0]
+        >>> o = s.parts[1].getElementsByClass('Measure')[2][0]
         >>> while o:
         ...    print(o)
         ...    o = o.previous()
@@ -4957,7 +4957,7 @@ class Test(unittest.TestCase):
 #     def testPreviousA(self):
 #         from music21 import corpus
 #         s = corpus.parse('bwv66.6')
-#         o = s.parts[0].iter.getElementsByClass('Measure')[2][1]
+#         o = s.parts[0].getElementsByClass('Measure')[2][1]
 #         i = 20
 #         while o and i:
 #             print(o)
diff --git a/music21/braille/segment.py b/music21/braille/segment.py
index 424adf6a3..246217db4 100644
--- a/music21/braille/segment.py
+++ b/music21/braille/segment.py
@@ -2066,8 +2066,8 @@ def prepareBeamedNotes(music21Measure):
     def beamStopFilter(el, unused):
         return el.beams.getByNumber(1).type == 'stop'
 
-    allStartIter = allNotes.iter.addFilter(withBeamFilter).addFilter(beamStartFilter)
-    allStopIter = allNotes.iter.addFilter(withBeamFilter).addFilter(beamStopFilter)
+    allStartIter = allNotes.iter().addFilter(withBeamFilter).addFilter(beamStartFilter)
+    allStopIter = allNotes.iter().addFilter(withBeamFilter).addFilter(beamStopFilter)
 
     if len(allStartIter) != len(allStopIter):
         environRules.warn('Incorrect beaming: number of start notes != to number of stop notes.')
diff --git a/music21/clef.py b/music21/clef.py
index d53540d35..d7cdc7bbd 100644
--- a/music21/clef.py
+++ b/music21/clef.py
@@ -934,7 +934,7 @@ def bestClef(streamObj: 'music21.stream.Stream',
     totalNotes = 0
     totalHeight = 0
 
-    sIter = streamObj.recurse() if recurse else streamObj.iter
+    sIter = streamObj.recurse() if recurse else streamObj.iter()
 
     notes = sIter.getElementsByClass('GeneralNote')
 
diff --git a/music21/figuredBass/checker.py b/music21/figuredBass/checker.py
index 96182713d..322f7de07 100644
--- a/music21/figuredBass/checker.py
+++ b/music21/figuredBass/checker.py
@@ -179,7 +179,7 @@ def correlateHarmonies(currentMapping, music21Part):
 
     for offsets in sorted(currentMapping.keys()):
         (initOffset, endTime) = offsets
-        notesInRange = music21Part.flat.iter.getElementsByClass('GeneralNote').getElementsByOffset(
+        notesInRange = music21Part.flat.getElementsByClass('GeneralNote').getElementsByOffset(
             initOffset, offsetEnd=endTime,
             includeEndBoundary=False, mustFinishInSpan=False,
             mustBeginInSpan=False, includeElementsThatEndAtStart=False)
@@ -250,7 +250,7 @@ def checkSinglePossibilities(music21Stream, functionToApply, color="#FF0000", de
         for partNumberTuple in vlm_violations:
             for partNumber in partNumberTuple:
                 if color is not None:
-                    noteA = allParts[partNumber - 1].iter.getElementsByOffset(
+                    noteA = allParts[partNumber - 1].getElementsByOffset(
                         initOffset,
                         initOffset,
                         mustBeginInSpan=False)[0]
@@ -320,9 +320,9 @@ def checkConsecutivePossibilities(music21Stream, functionToApply, color="#FF0000
         for partNumberTuple in vlm_violations:
             for partNumber in partNumberTuple:
                 if color is not None:
-                    noteA = allParts[partNumber - 1].iter.getElementsByOffset(
+                    noteA = allParts[partNumber - 1].getElementsByOffset(
                         initOffsetA, initOffsetA, mustBeginInSpan=False).first()
-                    noteB = allParts[partNumber - 1].iter.getElementsByOffset(
+                    noteB = allParts[partNumber - 1].getElementsByOffset(
                         initOffsetB, initOffsetB, mustBeginInSpan=False).first()
                     noteA.style.color = color
                     noteB.style.color = color
diff --git a/music21/graph/axis.py b/music21/graph/axis.py
index f541e7412..6abf36ee0 100644
--- a/music21/graph/axis.py
+++ b/music21/graph/axis.py
@@ -1107,7 +1107,7 @@ class QuarterLengthAxis(PositionAxis):
         elif self.client.recurse:
             sSrc = s.recurse()
         else:
-            sSrc = s.iter
+            sSrc = s.iter()
 
         sSrc = sSrc.getElementsByClass(self.client.classFilterList)
         # get all quarter lengths
diff --git a/music21/graph/plot.py b/music21/graph/plot.py
index a71eb4816..7198fc8d2 100644
--- a/music21/graph/plot.py
+++ b/music21/graph/plot.py
@@ -195,7 +195,7 @@ class PlotStreamMixin(prebase.ProtoM21Object):
         if self.recurse:
             sIter = self.streamObj.recurse()
         else:
-            sIter = self.streamObj.iter
+            sIter = self.streamObj.iter()
 
         if self.classFilterList:
             sIter = sIter.getElementsByClass(self.classFilterList)
diff --git a/music21/key.py b/music21/key.py
index 0dd523f0c..c61c8e9f9 100644
--- a/music21/key.py
+++ b/music21/key.py
@@ -21,6 +21,7 @@ import copy
 import re
 import unittest
 from typing import Union, Optional
+import warnings
 
 from music21 import base
 from music21 import exceptions21
@@ -72,7 +73,7 @@ def convertKeyStringToMusic21KeyString(textString):
 
 def sharpsToPitch(sharpCount):
     '''
-    Given a number a positive/negative number of sharps, return a Pitch
+    Given a positive/negative number of sharps, return a Pitch
     object set to the appropriate major key value.
 
     >>> key.sharpsToPitch(1)
@@ -251,6 +252,10 @@ class KeyException(exceptions21.Music21Exception):
     pass
 
 
+class KeyWarning(Warning):
+    pass
+
+
 # ------------------------------------------------------------------------------
 class KeySignature(base.Music21Object):
     '''
@@ -380,11 +385,51 @@ class KeySignature(base.Music21Object):
     def _reprInternal(self):
         return 'of ' + self._strDescription()
 
-    def asKey(self, mode='major'):
+    def asKey(self, mode: Optional[str] = None, tonic: Optional[str] = None):
         '''
-        return a `key.Key` object representing this KeySignature object as a key in the
-        given mode (default = major)
+        Return a `key.Key` object representing this KeySignature object as a key in the
+        given mode or in the given tonic. If `mode` is None, and `tonic` is not provided,
+        major is assumed. If both mode and tonic are provided, the tonic is ignored.
+
+        >>> ks = key.KeySignature(2)
+        >>> ks.asKey()
+        <music21.key.Key of D major>
+        >>> ks.asKey(mode='minor')
+        <music21.key.Key of b minor>
+
+        If `mode` is None, an attempt is made to solve for the mode:
+
+        >>> ks.asKey(tonic='A')
+        <music21.key.Key of A mixolydian>
+
+        But will raise `KeyException` if an impossible solution is requested:
+
+        >>> ks.asKey(tonic='D#')
+        Traceback (most recent call last):
+        music21.key.KeyException: Could not solve for mode from sharps=2, tonic=D#
+
+        Ionian and Aeolian are supplied instead of major or minor when deriving mode in this way:
+
+        >>> ks2 = key.KeySignature()
+        >>> ks2.asKey()
+        <music21.key.Key of C major>
+        >>> ks2.asKey(tonic='C')
+        <music21.key.Key of C ionian>
+
+        New in v7 -- `tonic` argument to solve for mode.
         '''
+        if mode is not None and tonic is not None:
+            warnings.warn(f'ignoring provided tonic: {tonic}', KeyWarning)
+        if mode is None and tonic is None:
+            mode = 'major'
+        if mode is None and tonic is not None:
+            majorSharpsToMode = {v: k for k, v in modeSharpsAlter.items()}
+            majorSharps = pitchToSharps(tonic)
+            try:
+                mode = majorSharpsToMode[self.sharps - majorSharps]
+            except KeyError as ke:
+                raise KeyException(
+                    f'Could not solve for mode from sharps={self.sharps}, tonic={tonic}') from ke
         mode = mode.lower()
         if mode not in modeSharpsAlter:
             raise KeyException(f'Mode {mode} is unknown')
@@ -1283,6 +1328,30 @@ class Test(unittest.TestCase):
         # k.tonalCertainty(method='correlationCoefficient')
         # s = corpus.parse('bwv48.3')
 
+    def testAsKey(self):
+        ks = KeySignature(2)
+
+        k = ks.asKey(mode=None, tonic=None)
+        self.assertEqual(k.mode, 'major')
+        self.assertEqual(k.tonicPitchNameWithCase, 'D')
+
+        k = ks.asKey(tonic='E')
+        self.assertEqual(k.mode, 'dorian')
+        self.assertEqual(k.tonicPitchNameWithCase, 'E')
+
+        expected = 'ignoring provided tonic: E'
+        with self.assertWarnsRegex(KeyWarning, expected) as cm:
+            # warn user we ignored their tonic
+            k = ks.asKey(mode='minor', tonic='E')
+        self.assertEqual(k.mode, 'minor')
+        self.assertEqual(k.tonicPitchNameWithCase, 'b')
+
+        expected = 'Could not solve for mode from sharps=2, tonic=A-'
+        with self.assertRaisesRegex(KeyException, expected) as cm:
+            k = ks.asKey(mode=None, tonic='A-')
+        # test exception chained from KeyError
+        self.assertIsInstance(cm.exception.__cause__, KeyError)
+
 
 # ------------------------------------------------------------------------------
 # define presented order in documentation
diff --git a/music21/layout.py b/music21/layout.py
index ca4ae9b5c..bf40ba494 100644
--- a/music21/layout.py
+++ b/music21/layout.py
@@ -861,7 +861,7 @@ class LayoutScore(stream.Opus):
 
 
         >>> #_DOCS_SHOW g = corpus.parse('luca/gloria')
-        >>> #_DOCS_SHOW m22 = g.parts[0].iter.getElementsByClass('Measure')[22]
+        >>> #_DOCS_SHOW m22 = g.parts[0].getElementsByClass('Measure')[22]
         >>> #_DOCS_SHOW m22.getElementsByClass('PageLayout').first().leftMargin = 204.0
         >>> #_DOCS_SHOW gl = layout.divideByPages(g)
         >>> #_DOCS_SHOW gl.getMarginsAndSizeForPageId(1)
@@ -1196,7 +1196,7 @@ class LayoutScore(stream.Opus):
                 f'No measures found in pageId {pageId}, systemId {systemId}, staffId {staffId}'
             )
 
-        allStaffLayouts = firstMeasureOfStaff.iter.getElementsByClass('StaffLayout')
+        allStaffLayouts = firstMeasureOfStaff.getElementsByClass('StaffLayout')
         if allStaffLayouts:
             # print('Got staffLayouts: ')
             for slTemp in allStaffLayouts:
@@ -1252,7 +1252,7 @@ class LayoutScore(stream.Opus):
 
         staffSize = staffSizeBase
 
-        allStaffLayouts = list(firstMeasureOfStaff.iter.getElementsByClass('StaffLayout'))
+        allStaffLayouts = list(firstMeasureOfStaff.getElementsByClass('StaffLayout'))
         if allStaffLayouts:
             # print('Got staffLayouts: ')
             staffLayoutObj = allStaffLayouts[0]
@@ -1302,7 +1302,7 @@ class LayoutScore(stream.Opus):
         thisStaff = self.pages[pageId].systems[systemId].staves[staffId]
 
         staffLayoutObject = None
-        allStaffLayoutObjects = list(thisStaff.flat.iter.getElementsByClass('StaffLayout'))
+        allStaffLayoutObjects = list(thisStaff.flat.getElementsByClass('StaffLayout'))
         if allStaffLayoutObjects:
             staffLayoutObject = allStaffLayoutObjects[0]
         if staffLayoutObject is None or staffLayoutObject.hidden is None:
@@ -1477,7 +1477,7 @@ class LayoutScore(stream.Opus):
                 # first system is hidden, thus has no width information
                 for j in range(1, len(thisSystemStaves)):
                     searchOtherStaffForWidth = thisSystemStaves[j]
-                    searchIter = searchOtherStaffForWidth.iter
+                    searchIter = searchOtherStaffForWidth.iter()
                     searchOtherStaffMeasure = searchIter.getElementsByClass('Measure')[i]
                     if searchOtherStaffMeasure.layoutWidth is not None:
                         currentWidth = searchOtherStaffMeasure.layoutWidth
diff --git a/music21/lily/translate.py b/music21/lily/translate.py
index 766edea41..b9afe79f9 100644
--- a/music21/lily/translate.py
+++ b/music21/lily/translate.py
@@ -396,7 +396,7 @@ class LilypondConverter:
 
         # Also get the variants, and the total number of measures here and make start each
         # staff context with { \stopStaff s1*n} where n is the number of measures.
-        if hasattr(scoreIn, 'parts') and scoreIn.iter.parts:  # or has variants
+        if hasattr(scoreIn, 'parts') and scoreIn.iter().parts:  # or has variants
             if scoreIn.recurse().variants:
                 lpPartsAndOssiaInit = self.lyPartsAndOssiaInitFromScore(scoreIn)
                 lpGroupedMusicList = self.lyGroupedMusicListFromScoreWithParts(
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index 0cdb7a11b..f84571167 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -2318,7 +2318,7 @@ def packetStorageFromSubstreamList(
         subs = subs.flat
 
         # get a first instrument; iterate over rest
-        instrumentStream = subs.iter.getElementsByClass('Instrument')
+        instrumentStream = subs.getElementsByClass('Instrument')
 
         # if there is an Instrument object at the start, make instObj that instrument
         # this may be a Conductor object if prepareStreamForMidi() was run
@@ -2946,10 +2946,10 @@ class Test(unittest.TestCase):
 
         # User error
         iObjs[0].midiChannel = 100
-        msg = 'Harpsichord specified 1-indexed MIDI channel 101 but '
-        msg += 'acceptable channels were [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16].'
-        msg += 'Defaulting to channel 1.'
-        with self.assertWarns(TranslateWarning, msg=msg):
+        want = 'Harpsichord specified 1-indexed MIDI channel 101 but '
+        want += r'acceptable channels were \[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 15, 16\]. '
+        want += 'Defaulting to channel 1.'
+        with self.assertWarnsRegex(TranslateWarning, want):
             channelByInstrument, channelsDynamic = channelInstrumentData(s)
         self.assertEqual(channelByInstrument.keys(), set(inst.midiProgram for inst in iObjs))
         self.assertSetEqual(set(channelByInstrument.values()), {1, 11, 12, 13, 14, 15})
@@ -3823,9 +3823,9 @@ class Test(unittest.TestCase):
         event.channel = 10
         event.type = midiModule.ChannelVoiceMessages.PROGRAM_CHANGE
 
-        msg = 'Unable to determine instrument from '
-        msg += '<music21.midi.MidiEvent PROGRAM_CHANGE, track=0, channel=10, data=0>'
-        with self.assertWarns(TranslateWarning, msg=msg):
+        expected = 'Unable to determine instrument from '
+        expected += '<music21.midi.MidiEvent PROGRAM_CHANGE, track=None, channel=10, data=0>'
+        with self.assertWarnsRegex(TranslateWarning, expected):
             i = midiEventsToInstrument(event)
             self.assertIsNone(i.instrumentName)
 
diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py
index 57f309b56..349ca424a 100644
--- a/music21/musicxml/m21ToXml.py
+++ b/music21/musicxml/m21ToXml.py
@@ -1968,18 +1968,20 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
           <part-group number="1" type="stop" />
         </part-list>
 
-        And with a piano staff, to observe that only one <part> tag has both "start" and "stop":
+        Multi-staff parts (such as piano staves), should NOT receive `<part-group>` tags,
+        since they are joined by `<staff>` tags:
 
         >>> cpe = corpus.parse('cpebach')
         >>> SX = musicxml.m21ToXml.ScoreExporter(cpe)
         >>> SX.scorePreliminaries()
         >>> SX.parsePartlikeScore()
+        >>> SX.joinPartStaffs()
 
         >>> mxPartList = SX.setPartList()
         >>> SX.dump(mxPartList)
         <part-list>
-          <part-group number="1" type="start">...
-          <part-group number="1" type="stop" />
+          <score-part id="P1">...
+          </score-part>
         </part-list>
         '''
 
@@ -1998,6 +2000,8 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
             p = pex.stream
             # check for first
             for sg in staffGroups:
+                if sg in self.joinedGroups:
+                    continue
                 if sg.isFirst(p):
                     mxPartGroup = self.staffGroupToXmlPartGroup(sg)
                     mxPartGroup.set('number', str(partGroupIndex))
@@ -2011,10 +2015,10 @@ class ScoreExporter(XMLExporterBase, PartStaffExporterMixin):
             # check for last
             activeIndex = None
             for sg in staffGroups:
+                if sg in self.joinedGroups:
+                    continue
                 # Handle last part in the StaffGroup
-                # as well as the first part in the StaffGroup if it was joined to the others
-                # In that latter case, we need the <part-group type="stop" /> on the FIRST m21 part
-                if (sg.isLast(p) or (sg in self.joinedGroups and sg.isFirst(p))):
+                if sg.isLast(p):
                     # find the spanner in the dictionary already-assigned
                     for key, value in partGroupIndexRef.items():
                         if value is sg:
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index dfcbfa9cf..65a7d3725 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -2201,9 +2201,12 @@ class MeasureParser(XMLParserBase):
         self.nLast = None  # for adding notes to spanners.
 
         # Sibelius 7.1 only puts a <voice> tag on the
-        # first note of a chord, so we need to make sure
-        # that we keep track of the last voice...
-        self.chordVoice = None
+        # first note of a chord, and MuseScore doesn't put one
+        # on <forward> elements for hidden rests, so we need to make sure
+        # that we keep track of the last voice.
+        # there is an effort to translate the voice text to an int, but if that fails (unlikely)
+        # we store whatever we find
+        self.lastVoice = None
         self.fullMeasureRest = False
 
         # for keeping track of full-measureRests.
@@ -2385,7 +2388,7 @@ class MeasureParser(XMLParserBase):
                     meth(mxObj)
 
         if self.useVoices is True:
-            for v in self.stream.iter.voices:
+            for v in self.stream.iter().voices:
                 if v:  # do not bother with empty voices
                     # the musicDataMethods use insertCore, thus the voices need to run
                     # coreElementsChanged
@@ -2563,7 +2566,7 @@ class MeasureParser(XMLParserBase):
                     vIndex = int(vIndex)
                 except ValueError:
                     pass
-                self.chordVoice = vIndex
+                self.lastVoice = vIndex
 
         if isChord is True:  # and isRest is False...?
             n = None  # fo linting
@@ -4343,6 +4346,10 @@ class MeasureParser(XMLParserBase):
             return
 
         mxVoice = mxElement.find('voice')
+
+        # MuseScore doesn't write `<voice>` children on `<forward>` elements,
+        # and Sibelius 7.1 skips it on subsequent chord members, so this might be None
+        # no matter: go on to findM21VoiceFromXmlVoice()
         thisVoice = self.findM21VoiceFromXmlVoice(mxVoice)
         if thisVoice is not None:
             insertStream = thisVoice
@@ -4354,7 +4361,7 @@ class MeasureParser(XMLParserBase):
         '''
         m = self.stream
         if not textStripValid(mxVoice):
-            useVoice = self.chordVoice
+            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... '
@@ -4362,6 +4369,10 @@ class MeasureParser(XMLParserBase):
                 useVoice = 1
         else:
             useVoice = mxVoice.text.strip()
+            try:
+                self.lastVoice = int(useVoice)
+            except ValueError:
+                self.lastVoice = useVoice
 
         thisVoice = None
         if useVoice in self.voicesById:
@@ -6861,6 +6872,25 @@ class Test(unittest.TestCase):
         self.assertEqual(len(lh_last.voices), 0)
         self.assertEqual([r.style.hideObjectOnPrint for r in lh_last[note.Rest]], [False] * 3)
 
+    def testHiddenRestImpliedVoice(self):
+        '''
+        MuseScore expects readers to infer the voice context surrounding
+        a <forward> tag.
+        '''
+        from xml.etree.ElementTree import fromstring as EL
+        elStr = '<measure><note><rest/><duration>20160</duration><voice>1</voice></note>'
+        elStr += '<backup><duration>20160</duration></backup>'
+        elStr += '<note><rest/><duration>10080</duration><voice>non-integer-value</voice></note>'
+        elStr += '<forward><duration>10080</duration></forward></measure>'
+        mxMeasure = EL(elStr)
+        MP = MeasureParser(mxMeasure=mxMeasure)
+        MP.parse()
+
+        self.assertEqual(len(MP.stream.voices), 2)
+        self.assertEqual(len(MP.stream.voices[0].elements), 1)
+        self.assertEqual(len(MP.stream.voices[1].elements), 2)
+        self.assertEqual(MP.stream.voices[1].id, 'non-integer-value')
+
     def testMultiDigitEnding(self):
         from music21 import converter
         from music21.musicxml import testPrimitive
diff --git a/music21/repeat.py b/music21/repeat.py
index b865eb493..19f15561b 100644
--- a/music21/repeat.py
+++ b/music21/repeat.py
@@ -53,7 +53,7 @@ class RepeatMark:
     >>> s = stream.Stream()
     >>> s.append(note.Note())
     >>> s.append(PartialRepeat())
-    >>> repeats = s.iter.getElementsByClass(repeat.RepeatMark)
+    >>> repeats = s.getElementsByClass(repeat.RepeatMark)
     >>> if repeats:
     ...    print('Stream has %s repeat(s) in it' % (len(repeats)))
     Stream has 1 repeat(s) in it
@@ -1715,7 +1715,7 @@ class Expander:
         '''
         post = []
         for i, m in enumerate(streamObj):
-            for e in m.iter.getElementsByClass('RepeatExpression'):
+            for e in m.getElementsByClass('RepeatExpression'):
                 if (target in e.classes
                         or (not isinstance(target, str) and isinstance(e, target))):
                     post.append(i)
diff --git a/music21/scale/__init__.py b/music21/scale/__init__.py
index ab859b5f1..22e827311 100644
--- a/music21/scale/__init__.py
+++ b/music21/scale/__init__.py
@@ -289,6 +289,15 @@ class AbstractScale(Scale):
         >>> abstractScale._net.realizePitch('D5')
         [<music21.pitch.Pitch D5>, <music21.pitch.Pitch F#5>,
          <music21.pitch.Pitch A#5>, <music21.pitch.Pitch D6>]
+
+        Also possible with implicit octaves:
+
+        >>> abstract_scale = scale.AbstractScale()
+        >>> abstract_scale.buildNetworkFromPitches(['C', 'F'])
+        >>> abstract_scale.octaveDuplicating
+        True
+        >>> abstract_scale._net.realizePitch('G')
+        [<music21.pitch.Pitch G4>, <music21.pitch.Pitch C5>, <music21.pitch.Pitch G5>]
         '''
         pitchListReal = []
         for p in pitchList:
@@ -321,6 +330,8 @@ class AbstractScale(Scale):
                 self.octaveDuplicating = False
         else:
             p = copy.deepcopy(pitchList[0])
+            if p.octave is None:
+                p.octave = p.implicitOctave
             if pitchList[-1] > pitchList[0]:  # ascending
                 while p.ps < pitchList[-1].ps:
                     p.octave += 1
diff --git a/music21/search/base.py b/music21/search/base.py
index 8310295de..ff3f59ab0 100644
--- a/music21/search/base.py
+++ b/music21/search/base.py
@@ -208,7 +208,7 @@ class StreamSearcher:
             if self.recurse:
                 thisStreamIterator = self.streamSearch.recurse()
             else:
-                thisStreamIterator = self.streamSearch.iter
+                thisStreamIterator = self.streamSearch.iter()
 
             if self.filterNotesAndRests:
                 thisStreamIterator = thisStreamIterator.addFilter(
diff --git a/music21/serial.py b/music21/serial.py
index 836c74244..019116abb 100644
--- a/music21/serial.py
+++ b/music21/serial.py
@@ -20,6 +20,7 @@ Serial searching methods that were previously here have been moved to `alpha.sea
 import unittest
 import copy
 from typing import Union, List, Any
+import warnings
 
 from music21 import exceptions21
 
@@ -610,7 +611,7 @@ class ToneRow(stream.Stream):
         to another, the second specified in the argument. Each transformation is given as a tuple
         of the transformation type and index.
 
-        See :meth:`~music21.serial.originalCenteredTransformation` for an
+        See :meth:`~music21.serial.ToneRow.originalCenteredTransformation` for an
         explanation of this convention.
 
         >>> chromatic = serial.pcToToneRow(       [2, 3, 4, 5, 6, 7, 8, 9, 'A', 'B',   0, 1])
@@ -1038,52 +1039,60 @@ class TwelveToneRow(ToneRow):
         else:
             return True
 
-    def areCombinatorial(self, transType1, index1, transType2, index2, convention):
+    def areCombinatorial(self, transType1, index1, transType2, index2, unused_convention=None):
         '''
         Describes whether or not two transformations of a twelve-tone row are combinatorial.
 
         The first and second arguments describe one transformation, while the third and fourth
-        describe another. One of the zero-centered or original-centered conventions for tone row
-        transformations must be specified in the last argument; see
-        :meth:`~music21.serial.ToneRow.zeroCenteredTransformation` and
-        :meth:`~music21.serial.ToneRow.originalCenteredTransformation` explanations
-        of these conventions.
+        describe another.
+
+        First, let's take a row we know to have a combinatoriality pair:
 
         >>> moses = serial.getHistoricalRowByName('SchoenbergMosesAron')
         >>> moses.pitchClasses()
         [9, 10, 4, 2, 3, 1, 7, 5, 6, 8, 11, 0]
-        >>> moses.areCombinatorial('P', 1, 'I', 4, 'zero')
+
+        Combinatoriality holds here between P0 and I3
+
+        >>> moses.areCombinatorial('P', 0, 'I', 3)
+        True
+
+        And a combinatorial pair like this between P0 and I3 will also hold
+        if you modify both rows in the same way, e.g.
+        if you transpose both by the same amount
+
+        >>> moses.areCombinatorial('P', 1, 'I', 4)
+        True
+
+        or if you retrograde both
+
+        >>> moses.areCombinatorial('R', 1, 'RI', 4)
         True
-        >>> moses.areCombinatorial('R', 5, 'RI', 6, 'original')
+
+        Any modification made to one row form and not the other means all bets are off
+
+        >>> moses.areCombinatorial('R', 6, 'RI', 4)
         False
+
+        Changed in v.7 -- `convention` is no longer necessary and no longer used.
+        Renamed to `unused_convention` and defaults None; to be removed in v.8.
         '''
         if self.isTwelveToneRow() is False:
             raise SerialException('Combinatoriality applies only to twelve-tone rows.')
 
-        if convention == 'zero':
-            testRow = []
-            trans1 = self.zeroCenteredTransformation(transType1, index1)
-            pitches1 = trans1.pitchClasses()
-            trans2 = self.zeroCenteredTransformation(transType2, index2)
-            pitches2 = trans2.pitchClasses()
-            for i in range(6):
-                testRow.append(pitches1[i])
-            for i in range(6):
-                testRow.append(pitches2[i])
-            return pcToToneRow(testRow).isTwelveToneRow()
-        elif convention == 'original':
-            testRow = []
-            trans1 = self.originalCenteredTransformation(transType1, index1)
-            pitches1 = trans1.pitchClasses()
-            trans2 = self.originalCenteredTransformation(transType2, index2)
-            pitches2 = trans2.pitchClasses()
-            for i in range(6):
-                testRow.append(pitches1[i])
-            for i in range(6):
-                testRow.append(pitches2[6 + i])
-            return pcToToneRow(testRow).isTwelveToneRow()
-        else:
-            raise SerialException("Invalid convention - choose 'zero' or 'original'.")
+        if unused_convention is not None:
+            # TODO: remove in v.8
+            warnings.warn('convention is unnecessary and will be removed in v.8')
+
+        # choice of convention does not matter
+        trans1 = self.zeroCenteredTransformation(transType1, index1)
+        trans2 = self.zeroCenteredTransformation(transType2, index2)
+
+        pitches1 = trans1.pitchClasses()
+        pitches2 = trans2.pitchClasses()
+        testRow = pitches1[:6] + pitches2[:6]
+
+        return pcToToneRow(testRow).isTwelveToneRow()
 
 
 class HistoricalTwelveToneRow(TwelveToneRow):
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 314b2f099..87c1c2158 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -121,8 +121,9 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
     its :attr:`~music21.base.Music21Object.offset`
     property. One special case is when every such object, such as a newly created
     one, has no offset. Then, so long as the entire list is not composed of
-    non-Measure Stream subclasses like Parts or Voices, each element is appended,
-    creating a sequence of elements in time, rather than synchrony.
+    non-Measure Stream subclasses representing synchrony like Parts or Voices,
+    each element is appended, creating a sequence of elements in time,
+    rather than synchrony.
 
     Other arguments and keywords are ignored, but are
     allowed so that subclassing the Stream is easier.
@@ -166,8 +167,8 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
     >>> s.first()
     <music21.meter.TimeSignature 4/4>
 
-    New in v7 -- providing a list of objects or Measures (but not other Stream
-    subclasses such as Parts) now positions sequentially, i.e. appends:
+    New in v7 -- providing a list of objects or Measures or Scores (but not other Stream
+    subclasses such as Parts or Voices) now positions sequentially, i.e. appends:
 
     >>> s2 = stream.Measure([note.Note(), note.Note(), bar.Barline()])
     >>> s2.show('text')
@@ -197,11 +198,11 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
     Create nested streams in one fell swoop:
 
-    >>> s5 = stream.Score(stream.Part(stream.Measure(note.Note())))
+    >>> s5 = stream.Score(stream.Part(stream.Measure(chord.Chord('C2 A2'))))
     >>> s5.show('text')
     {0.0} <music21.stream.Part 0x...>
         {0.0} <music21.stream.Measure 0 offset=0.0>
-            {0.0} <music21.note.Note C>
+            {0.0} <music21.chord.Chord C2 A2>
     '''
     # this static attributes offer a performance boost over other
     # forms of checking class
@@ -278,18 +279,21 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         if givenElements is None:
             return
 
-        if isinstance(givenElements, Stream) or not common.isIterable(givenElements):
+        if isinstance(givenElements, base.Music21Object) or not common.isIterable(givenElements):
             givenElements = [givenElements]
 
         # Append rather than insert if every offset is 0.0
-        # but not if every element is a stream subclass other than a Measure
-        # (i.e. Opus, Score, Part, or Voice)
+        # but not if every element is a stream subclass other than a Measure or Score
+        # (i.e. Part or Voice generally, but even Opus theoretically)
+        # because these classes usually represent synchrony
         append: bool = False
         try:
             append = all(e.offset == 0.0 for e in givenElements)
         except AttributeError:
             pass  # appropriate failure will be raised by coreGuardBeforeAddElement()
-        if append and all((e.isStream and not e.isMeasure) for e in givenElements):
+        if append and all(
+                (e.isStream and e.classSet.isdisjoint((Measure, Score)))
+                for e in givenElements):
             append = False
 
         for e in givenElements:
@@ -560,7 +564,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         Return the first element of a Stream.  (Added for compatibility with StreamIterator)
         Or None if the Stream is empty.
 
-        Unlike s.iter.first(), which is a significant performance gain, s.first() is the
+        Unlike s.iter().first(), which is a significant performance gain, s.first() is the
         same speed as s[0], except for not raising an IndexError.
 
         >>> nC = note.Note('C4')
@@ -934,7 +938,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         {0.0} <music21.clef.BassClef>
         {0.0} <music21.note.Note D#>
         '''
-        clefList = self.iter.getElementsByClass('Clef').getElementsByOffset(0)
+        clefList = self.getElementsByClass('Clef').getElementsByOffset(0)
         # casting to list added 20microseconds...
         return clefList.first()
 
@@ -997,7 +1001,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         True
         '''
         # there could be more than one
-        tsList = self.iter.getElementsByClass('TimeSignature').getElementsByOffset(0)
+        tsList = self.getElementsByClass('TimeSignature').getElementsByOffset(0)
         # environLocal.printDebug([
         #    'matched Measure classes of type TimeSignature', tsList, len(tsList)])
         # only return timeSignatures at offset = 0.0
@@ -1048,7 +1052,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         True
         '''
         try:
-            return next(self.iter.getElementsByClass('KeySignature').getElementsByOffset(0))
+            return next(self.iter().getElementsByClass('KeySignature').getElementsByOffset(0))
         except StopIteration:
             return None
 
@@ -1592,11 +1596,11 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 shiftDur += matchDuration
                 if shiftDur != 0.0:
                     # can this be done with recurse???
-                    for e in self.iter.getElementsByOffset(shiftedRegionStart,
-                                                           shiftedRegionEnd,
-                                                           includeEndBoundary=False,
-                                                           mustFinishInSpan=False,
-                                                           mustBeginInSpan=True):
+                    for e in self.getElementsByOffset(shiftedRegionStart,
+                                                        shiftedRegionEnd,
+                                                        includeEndBoundary=False,
+                                                        mustFinishInSpan=False,
+                                                        mustBeginInSpan=True):
 
                         elementOffset = self.elementOffset(e)
                         self.coreSetElementOffset(e, elementOffset - shiftDur)
@@ -1693,7 +1697,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> len(s)
         5
         '''
-        elFilter = self.iter.getElementsByClass(classFilterList)
+        elFilter = self.iter().getElementsByClass(classFilterList)
         self._removeIteration(elFilter)
 
     def removeByNotOfClass(self, classFilterList):
@@ -1712,7 +1716,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> len(s.notes)
         0
         '''
-        elFilter = self.iter.getElementsNotOfClass(classFilterList)
+        elFilter = self.iter().getElementsNotOfClass(classFilterList)
         return self._removeIteration(elFilter)
 
     def _deepcopySubclassable(self, memo=None, ignoreAttributes=None, removeFromIgnore=None):
@@ -2200,7 +2204,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         '''
         # could use duration of Note to get end offset span
         targets = list(
-            self.iter.getElementsByOffset(
+            self.getElementsByOffset(
                 offset,
                 offset + noteOrChord.quarterLength,  # set end to dur of supplied
                 includeEndBoundary=False,
@@ -2673,7 +2677,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         if recurse:
             sIter = self.recurse()
         else:
-            sIter = self.iter
+            sIter = self.iter()
 
         for el in sIter:
             if el.derivation is not None:
@@ -3270,7 +3274,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         Added in v6.7.1 -- recurse
         '''
-        sIterator = self.iter if not recurse else self.recurse()
+        sIterator = self.iter() if not recurse else self.recurse()
         if classFilter is not None:
             sIterator = sIterator.addFilter(filters.ClassFilter(classFilter))
         for el in sIterator:
@@ -3355,7 +3359,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> len(foundList)
         25
         '''
-        return self.iter.getElementsByClass(classFilterList, returnClone=False)
+        return self.iter().getElementsByClass(classFilterList, returnClone=False)
 
     def getElementsNotOfClass(self, classFilterList) -> iterator.StreamIterator:
         '''
@@ -3388,7 +3392,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         >>> len(found)
         25
         '''
-        return self.iter.getElementsNotOfClass(classFilterList, returnClone=False)
+        return self.iter().getElementsNotOfClass(classFilterList, returnClone=False)
 
     def getElementsByGroup(self, groupFilterList) -> iterator.StreamIterator:
         '''
@@ -3417,7 +3421,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         OMIT_FROM_DOCS
         # TODO: group comparisons are not YET case insensitive.
         '''
-        return self.iter.getElementsByGroup(groupFilterList, returnClone=False)
+        return self.iter().getElementsByGroup(groupFilterList, returnClone=False)
 
     def getElementById(self, elementId) -> Optional[base.Music21Object]:
         '''
@@ -3452,7 +3456,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         Changed in v7. -- remove classFilter.
         '''
-        sIterator = self.iter.addFilter(filters.IdFilter(elementId))
+        sIterator = self.iter().addFilter(filters.IdFilter(elementId))
         for e in sIterator:
             return e
         return None
@@ -3694,7 +3698,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
 
         Changed in v5.5: all arguments changing behavior are keyword only.
         '''
-        sIterator = self.iter.getElementsByOffset(
+        sIterator = self.iter().getElementsByOffset(
             offsetStart=offsetStart,
             offsetEnd=offsetEnd,
             includeEndBoundary=includeEndBoundary,
@@ -3797,7 +3801,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         offset = opFrac(offset)
         nearestTrailSpan = offset  # start with max time
 
-        sIterator = self.iter
+        sIterator = self.iter()
         if classList:
             sIterator = sIterator.getElementsByClass(classList)
 
@@ -3885,7 +3889,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         offset = opFrac(offset)
         nearestTrailSpan = offset  # start with max time
 
-        sIterator = self.iter
+        sIterator = self.iter()
         if classList:
             sIterator = sIterator.getElementsByClass(classList)
 
@@ -5164,7 +5168,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         if recurse:
             sIter = self.recurse()
         else:
-            sIter = self.iter
+            sIter = self.iter()
 
         post = sIter.getElementsByClass('Instrument').stream()
         if post:
@@ -10682,7 +10686,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
         voiceIds = []
 
         if self.hasMeasures():
-            for m in self.iter.getElementsByClass('Measure'):
+            for m in self.getElementsByClass('Measure'):
                 mVoices = m.voices
                 mVCount = len(mVoices)
                 if not countById:
@@ -10911,7 +10915,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
             p.mergeElements(self, classFilterList=('Instrument',))
 
         if self.hasMeasures():
-            for m in self.iter.getElementsByClass('Measure'):
+            for m in self.getElementsByClass('Measure'):
                 if m.hasVoices():
                     doOneMeasureWithVoices(m)
                 # if a measure does not have voices, simply populate
@@ -11558,7 +11562,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object):
                 # get target offset relative to Stream
                 oInStream = vStart + e.getOffsetBySite(v.containedSite)
                 # get all elements at this offset, force a class match
-                targets = self.iter.getElementsByOffset(oInStream).getElementsByClass(e.classes[0])
+                targets = self.getElementsByOffset(oInStream).getElementsByClass(e.classes[0])
                 # only replace if we match the start
                 if targets:
                     targetsMatched += 1
@@ -13466,11 +13470,11 @@ class Score(Stream):
         post.mergeAttributes(self)
 
         # get all things in the score that are not Parts
-        for e in self.iter.getElementsNotOfClass('Part'):
+        for e in self.iter().getElementsNotOfClass('Part'):
             eNew = copy.deepcopy(e)  # assume that this is needed
             post.insert(self.elementOffset(e), eNew)
 
-        for p in self.iter.getElementsByClass('Part'):
+        for p in self.getElementsByClass('Part'):
             # get spanners at highest level, not by Part
             post.insert(0, p.expandRepeats(copySpanners=False))
 
@@ -13500,7 +13504,7 @@ class Score(Stream):
         This method is smart and does not assume that all Parts
         have measures with identical offsets.
         '''
-        parts = self.iter.parts
+        parts = self.iter().parts
         if not parts:
             return Stream.measureOffsetMap(self, classFilterList)
         # else:
@@ -13771,7 +13775,7 @@ class Score(Stream):
 
         # do not assume that we have parts here
         if self.hasPartLikeStreams():
-            for s in returnStream.iter.getElementsByClass('Stream'):
+            for s in returnStream.getElementsByClass('Stream'):
                 # process all component Streams inPlace
                 s.makeNotation(meterStream=meterStream,
                                refStreamOrTimeRange=refStreamOrTimeRange,
@@ -13819,7 +13823,7 @@ class Opus(Stream):
         ['1', '2', '3']
         '''
         post = []
-        for s in self.iter.getElementsByClass('Score'):
+        for s in self.getElementsByClass('Score'):
             post.append(s.metadata.number)
         return post
 
@@ -13840,7 +13844,7 @@ class Opus(Stream):
         >>> s.metadata.alternativeTitle
         'Tenor'
         '''
-        for s in self.iter.getElementsByClass('Score'):
+        for s in self.getElementsByClass('Score'):
             match, unused_field = s.metadata.search(opusMatch, 'number')
             if match:
                 return s
diff --git a/music21/stream/core.py b/music21/stream/core.py
index f3adddbd4..251590f72 100644
--- a/music21/stream/core.py
+++ b/music21/stream/core.py
@@ -399,7 +399,7 @@ class StreamCoreMixin:
         Traceback (most recent call last):
         music21.exceptions21.StreamException: this Stream cannot be contained within itself
 
-        >>> s.append(s.iter)
+        >>> s.append(s.iter())
         Traceback (most recent call last):
         music21.exceptions21.StreamException: cannot insert StreamIterator into a Stream
         Iterate over it instead (User's Guide chs. 6 and 26)
@@ -694,7 +694,7 @@ class StreamCoreMixin:
         if recurse is True:
             sIter = self.recurse()
         else:
-            sIter = self.iter
+            sIter = self.iter()
 
         collectList = []
         for el in list(sIter):
diff --git a/music21/stream/filters.py b/music21/stream/filters.py
index 1d29feef5..cdfaef4b1 100644
--- a/music21/stream/filters.py
+++ b/music21/stream/filters.py
@@ -93,7 +93,7 @@ class IsFilter(StreamFilter):
     `.numToFind` is used so that once all elements are found, the iterator can short circuit.
 
 
-    >>> for el in s.iter.addFilter(isFilter):
+    >>> for el in s.iter().addFilter(isFilter):
     ...     print(el is n)
     True
 
@@ -109,7 +109,7 @@ class IsFilter(StreamFilter):
     >>> isFilter2.numToFind
     2
 
-    >>> for el in s.iter.addFilter(isFilter2):
+    >>> for el in s.iter().addFilter(isFilter2):
     ...     print(el)
     <music21.note.Note C#>
     <music21.note.Rest quarter>
@@ -148,14 +148,14 @@ class IsNotFilter(IsFilter):
     >>> n = note.Note('C#')
     >>> s.append(n)
     >>> s.append(note.Rest())
-    >>> for el in s.iter.addFilter(stream.filters.IsNotFilter(n)):
+    >>> for el in s.iter().addFilter(stream.filters.IsNotFilter(n)):
     ...     el
     <music21.key.KeySignature of 3 flats>
     <music21.note.Rest quarter>
 
     test that resetting works...
 
-    >>> for el in s.iter.addFilter(stream.filters.IsNotFilter(n)):
+    >>> for el in s.iter().addFilter(stream.filters.IsNotFilter(n)):
     ...     el
     <music21.key.KeySignature of 3 flats>
     <music21.note.Rest quarter>
@@ -169,7 +169,7 @@ class IsNotFilter(IsFilter):
     >>> s.append(n)
     >>> r = note.Rest()
     >>> s.append(r)
-    >>> for el in s.iter.addFilter(stream.filters.IsNotFilter([n, r])):
+    >>> for el in s.iter().addFilter(stream.filters.IsNotFilter([n, r])):
     ...     print(el)
     <music21.key.KeySignature of 3 flats>
     '''
diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py
index 9d9b0507c..15331b10c 100644
--- a/music21/stream/iterator.py
+++ b/music21/stream/iterator.py
@@ -164,6 +164,21 @@ class StreamIterator(prebase.ProtoM21Object):
 
         return f'for {streamClass}:{srcStreamId} @:{self.index}'
 
+    def __call__(self) -> _SIter:
+        '''
+        Temporary workaround to support both prior usage of `.iter`
+        and new recommended usage of `.iter()`.
+        During the period where `.iter` is still supported, even calling `.iter()`
+        (recommended) will retrieve the property `.iter` 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 __iter__(self):
         self.reset()
         return self
@@ -302,7 +317,7 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> s = stream.Stream()
         >>> s.insert(0, note.Note('F#'))
         >>> s.repeatAppend(note.Note('C'), 2)
-        >>> sI = s.iter
+        >>> sI = s.iter()
         >>> sI
         <music21.stream.iterator.StreamIterator for Stream:0x104743be0 @:0>
 
@@ -331,7 +346,7 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> s.insert(0, clef.TrebleClef())
         >>> s[0]
         <music21.clef.TrebleClef>
-        >>> s.iter.notes[0]
+        >>> s.iter().notes[0]
         <music21.note.Note F#>
 
         Demo of cleanupOnStop = True
@@ -371,11 +386,11 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> s = converter.parse('tinynotation: 3/4 c4 d e f g a', makeNotation=False)
         >>> len(s)
         7
-        >>> len(s.iter)
+        >>> len(s.iter())
         7
-        >>> len(s.iter.notes)
+        >>> len(s.iter().notes)
         6
-        >>> [n.name for n in s.iter.notes]
+        >>> [n.name for n in s.iter().notes]
         ['C', 'D', 'E', 'F', 'G', 'A']
         '''
         if self._len is not None:
@@ -492,7 +507,7 @@ class StreamIterator(prebase.ProtoM21Object):
         An Empty stream:
 
         >>> s = stream.Stream()
-        >>> s.iter.notes.first() is None
+        >>> s.iter().notes.first() is None
         True
         '''
         iter(self)
@@ -522,7 +537,7 @@ class StreamIterator(prebase.ProtoM21Object):
         Check on empty Stream:
 
         >>> s2 = stream.Stream()
-        >>> s2.iter.notes.last() is None
+        >>> s2.iter().notes.last() is None
         True
 
         Next has a different feature from first(), will start again from beginning.
@@ -607,7 +622,7 @@ class StreamIterator(prebase.ProtoM21Object):
 
         >>> s = converter.parse('tinynotation: 3/4 c4 d e f g a', makeNotation=False)
         >>> s.id = 'tn3/4'
-        >>> sI = s.iter
+        >>> sI = s.iter()
         >>> sI
         <music21.stream.iterator.StreamIterator for Part:tn3/4 @:0>
 
@@ -689,7 +704,7 @@ class StreamIterator(prebase.ProtoM21Object):
         cannot just call `type(StreamIterator.srcStream)()`
 
         >>> p = stream.Part()
-        >>> pi = p.iter
+        >>> pi = p.iter()
         >>> s = pi._newBaseStream()
         >>> s
         <music21.stream.Stream 0x1047eb2e8>
@@ -720,7 +735,7 @@ class StreamIterator(prebase.ProtoM21Object):
 
         In other words:
 
-        `s.getElementsByClass()` == `s.iter.getElementsByClass().stream()`
+        `s.getElementsByClass()` == `s.iter().getElementsByClass().stream()`
 
         >>> s = stream.Part()
         >>> s.insert(0, note.Note('C'))
@@ -729,7 +744,7 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> b = bar.Barline()
         >>> s.storeAtEnd(b)
 
-        >>> s2 = s.iter.getElementsByClass('Note').stream()
+        >>> s2 = s.iter().getElementsByClass('Note').stream()
         >>> s2.show('t')
         {0.0} <music21.note.Note C>
         {2.0} <music21.note.Note D>
@@ -738,7 +753,7 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> s2
         <music21.stream.Part ...>
 
-        >>> s3 = s.iter.stream()
+        >>> s3 = s.iter().stream()
         >>> s3.show('t')
         {0.0} <music21.note.Note C>
         {1.0} <music21.note.Rest quarter>
@@ -748,7 +763,7 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> s3.elementOffset(b, returnSpecial=True)
         <OffsetSpecial.AT_END>
 
-        >>> s4 = s.iter.getElementsByClass('Barline').stream()
+        >>> s4 = s.iter().getElementsByClass('Barline').stream()
         >>> s4.show('t')
         {0.0} <music21.bar.Barline type=regular>
 
@@ -909,7 +924,7 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> r = note.Rest()
         >>> s.append(r)
         >>> s.append(note.Note('D'))
-        >>> for el in s.iter.getElementsByClass('Rest'):
+        >>> for el in s.iter().getElementsByClass('Rest'):
         ...     print(el)
         <music21.note.Rest quarter>
 
@@ -921,14 +936,14 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> r.activeSite.id
         's2'
 
-        >>> for el in s.iter.getElementsByClass('Rest'):
+        >>> for el in s.iter().getElementsByClass('Rest'):
         ...     print(el.activeSite.id)
         s1
 
 
         Classes work in addition to strings...
 
-        >>> for el in s.iter.getElementsByClass(note.Rest):
+        >>> for el in s.iter().getElementsByClass(note.Rest):
         ...     print(el)
         <music21.note.Rest quarter>
 
@@ -995,13 +1010,13 @@ class StreamIterator(prebase.ProtoM21Object):
         ...     n = note.Note('G#')
         ...     n.offset = x * 3
         ...     a.insert(n)
-        >>> found = a.iter.getElementsNotOfClass(note.Note)
+        >>> found = a.iter().getElementsNotOfClass(note.Note)
         >>> len(found)
         10
-        >>> found = a.iter.getElementsNotOfClass('Rest')
+        >>> found = a.iter().getElementsNotOfClass('Rest')
         >>> len(found)
         4
-        >>> found = a.iter.getElementsNotOfClass(['Note', 'Rest'])
+        >>> found = a.iter().getElementsNotOfClass(['Note', 'Rest'])
         >>> len(found)
         0
 
@@ -1032,12 +1047,12 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> s1.append(n2)
         >>> s1.append(n3)
 
-        >>> tboneSubStream = s1.iter.getElementsByGroup('trombone')
+        >>> tboneSubStream = s1.iter().getElementsByGroup('trombone')
         >>> for thisNote in tboneSubStream:
         ...     print(thisNote.name)
         C
         D
-        >>> tubaSubStream = s1.iter.getElementsByGroup('tuba')
+        >>> tubaSubStream = s1.iter().getElementsByGroup('tuba')
         >>> for thisNote in tubaSubStream:
         ...     print(thisNote.name)
         D
@@ -1119,34 +1134,34 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> n2.duration.type = 'half'
         >>> n2.offset = 2
         >>> st1.insert(n2)
-        >>> out1 = list(st1.iter.getElementsByOffset(2))
+        >>> out1 = list(st1.iter().getElementsByOffset(2))
         >>> len(out1)
         1
         >>> out1[0].step
         'D'
-        >>> out2 = list(st1.iter.getElementsByOffset(1, 3))
+        >>> out2 = list(st1.iter().getElementsByOffset(1, 3))
         >>> len(out2)
         1
         >>> out2[0].step
         'D'
-        >>> out3 = list(st1.iter.getElementsByOffset(1, 3, mustFinishInSpan=True))
+        >>> out3 = list(st1.iter().getElementsByOffset(1, 3, mustFinishInSpan=True))
         >>> len(out3)
         0
-        >>> out4 = list(st1.iter.getElementsByOffset(1, 2))
+        >>> out4 = list(st1.iter().getElementsByOffset(1, 2))
         >>> len(out4)
         1
         >>> out4[0].step
         'D'
-        >>> out5 = list(st1.iter.getElementsByOffset(1, 2, includeEndBoundary=False))
+        >>> out5 = list(st1.iter().getElementsByOffset(1, 2, includeEndBoundary=False))
         >>> len(out5)
         0
-        >>> out6 = list(st1.iter.getElementsByOffset(1, 2, includeEndBoundary=False,
+        >>> out6 = list(st1.iter().getElementsByOffset(1, 2, includeEndBoundary=False,
         ...                                          mustBeginInSpan=False))
         >>> len(out6)
         1
         >>> out6[0].step
         'C'
-        >>> out7 = list(st1.iter.getElementsByOffset(1, 3, mustBeginInSpan=False))
+        >>> out7 = list(st1.iter().getElementsByOffset(1, 3, mustBeginInSpan=False))
         >>> len(out7)
         2
         >>> [el.step for el in out7]
@@ -1154,7 +1169,7 @@ class StreamIterator(prebase.ProtoM21Object):
 
         Note, that elements that end at the start offset are included if mustBeginInSpan is False
 
-        >>> out8 = list(st1.iter.getElementsByOffset(2, 4, mustBeginInSpan=False))
+        >>> out8 = list(st1.iter().getElementsByOffset(2, 4, mustBeginInSpan=False))
         >>> len(out8)
         2
         >>> [el.step for el in out8]
@@ -1162,7 +1177,7 @@ class StreamIterator(prebase.ProtoM21Object):
 
         To change this behavior set includeElementsThatEndAtStart=False
 
-        >>> out9 = list(st1.iter.getElementsByOffset(2, 4, mustBeginInSpan=False,
+        >>> out9 = list(st1.iter().getElementsByOffset(2, 4, mustBeginInSpan=False,
         ...                                          includeElementsThatEndAtStart=False))
         >>> len(out9)
         1
@@ -1175,10 +1190,10 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> a.repeatInsert(n, list(range(8)))
         >>> b = stream.Stream(id='b')
         >>> b.repeatInsert(a, [0, 3, 6])
-        >>> c = list(b.iter.getElementsByOffset(2, 6.9))
+        >>> c = list(b.iter().getElementsByOffset(2, 6.9))
         >>> len(c)
         2
-        >>> c = list(b.flat.iter.getElementsByOffset(2, 6.9))
+        >>> c = list(b.flat.iter().getElementsByOffset(2, 6.9))
         >>> len(c)
         10
 
@@ -1191,9 +1206,9 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> s.insert(0.0, c)
         >>> s.insert(0.0, ts)
         >>> s.insert(0.0, ks)
-        >>> len(list(s.iter.getElementsByOffset(0.0, mustBeginInSpan=True)))
+        >>> len(list(s.iter().getElementsByOffset(0.0, mustBeginInSpan=True)))
         3
-        >>> len(list(s.iter.getElementsByOffset(0.0, mustBeginInSpan=False)))
+        >>> len(list(s.iter().getElementsByOffset(0.0, mustBeginInSpan=False)))
         3
 
         On a :class:`~music21.stream.iterator.RecursiveIterator`,
@@ -1249,46 +1264,46 @@ class StreamIterator(prebase.ProtoM21Object):
 
         Same test as above, but with floats
 
-        >>> out1 = list(st1.iter.getElementsByOffset(2.0))
+        >>> out1 = list(st1.iter().getElementsByOffset(2.0))
         >>> len(out1)
         1
         >>> out1[0].step
         'D'
-        >>> out2 = list(st1.iter.getElementsByOffset(1.0, 3.0))
+        >>> out2 = list(st1.iter().getElementsByOffset(1.0, 3.0))
         >>> len(out2)
         1
         >>> out2[0].step
         'D'
-        >>> out3 = list(st1.iter.getElementsByOffset(1.0, 3.0, mustFinishInSpan=True))
+        >>> out3 = list(st1.iter().getElementsByOffset(1.0, 3.0, mustFinishInSpan=True))
         >>> len(out3)
         0
-        >>> out3b = list(st1.iter.getElementsByOffset(0.0, 3.001, mustFinishInSpan=True))
+        >>> out3b = list(st1.iter().getElementsByOffset(0.0, 3.001, mustFinishInSpan=True))
         >>> len(out3b)
         1
         >>> out3b[0].step
         'C'
-        >>> out3b = list(st1.iter.getElementsByOffset(1.0, 3.001, mustFinishInSpan=True,
+        >>> out3b = list(st1.iter().getElementsByOffset(1.0, 3.001, mustFinishInSpan=True,
         ...                                           mustBeginInSpan=False))
         >>> len(out3b)
         1
         >>> out3b[0].step
         'C'
 
-        >>> out4 = list(st1.iter.getElementsByOffset(1.0, 2.0))
+        >>> out4 = list(st1.iter().getElementsByOffset(1.0, 2.0))
         >>> len(out4)
         1
         >>> out4[0].step
         'D'
-        >>> out5 = list(st1.iter.getElementsByOffset(1.0, 2.0, includeEndBoundary=False))
+        >>> out5 = list(st1.iter().getElementsByOffset(1.0, 2.0, includeEndBoundary=False))
         >>> len(out5)
         0
-        >>> out6 = list(st1.iter.getElementsByOffset(1.0, 2.0, includeEndBoundary=False,
+        >>> out6 = list(st1.iter().getElementsByOffset(1.0, 2.0, includeEndBoundary=False,
         ...                                          mustBeginInSpan=False))
         >>> len(out6)
         1
         >>> out6[0].step
         'C'
-        >>> out7 = list(st1.iter.getElementsByOffset(1.0, 3.0, mustBeginInSpan=False))
+        >>> out7 = list(st1.iter().getElementsByOffset(1.0, 3.0, mustBeginInSpan=False))
         >>> len(out7)
         2
         >>> [el.step for el in out7]
@@ -1321,7 +1336,7 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> s.append(note.Note('C'))
         >>> s.append(note.Rest())
         >>> s.append(note.Note('D'))
-        >>> for el in s.iter.notes:
+        >>> for el in s.iter().notes:
         ...     print(el)
         <music21.note.Note C>
         <music21.note.Note D>
@@ -1338,7 +1353,7 @@ class StreamIterator(prebase.ProtoM21Object):
         >>> s.append(note.Note('C'))
         >>> s.append(note.Rest())
         >>> s.append(note.Note('D'))
-        >>> for el in s.iter.notesAndRests:
+        >>> for el in s.iter().notesAndRests:
         ...     print(el)
         <music21.note.Note C>
         <music21.note.Rest quarter>
@@ -1346,7 +1361,7 @@ class StreamIterator(prebase.ProtoM21Object):
 
         chained filters... (this makes no sense since notes is a subset of notesAndRests)
 
-        >>> for el in s.iter.notesAndRests.notes:
+        >>> for el in s.iter().notesAndRests.notes:
         ...     print(el)
         <music21.note.Note C>
         <music21.note.Note D>
@@ -1843,11 +1858,11 @@ class Test(unittest.TestCase):
         r = note.Rest()
         n = note.Note()
         s.append([r, n])
-        all_s = list(s.iter)
+        all_s = list(s.iter())
         self.assertEqual(len(all_s), 2)
         self.assertIs(all_s[0], r)
         self.assertIs(all_s[1], n)
-        s_notes = list(s.iter.notes)
+        s_notes = list(s.iter().notes)
         self.assertEqual(len(s_notes), 1)
         self.assertIs(s_notes[0], n)
 
@@ -1857,7 +1872,7 @@ class Test(unittest.TestCase):
         r = note.Rest()
         n = note.Note()
         s.append([r, n])
-        sIter = s.iter
+        sIter = s.iter()
         r0 = next(sIter)
         self.assertIs(r0, r)
 
diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py
index 1fe71ee48..894903136 100644
--- a/music21/stream/makeNotation.py
+++ b/music21/stream/makeNotation.py
@@ -123,7 +123,7 @@ def makeBeams(
         returnObj: stream.Measure
         mColl = [returnObj]  # store a list of measures for processing
     else:
-        mColl = list(returnObj.iter.getElementsByClass('Measure'))  # a list of measures
+        mColl = list(returnObj.getElementsByClass('Measure'))  # a list of measures
         if not mColl:
             raise stream.StreamException(
                 'cannot process a stream that is neither a Measure nor has no Measures')
@@ -476,7 +476,7 @@ def makeMeasures(
     # del clefList
     clefObj = srcObj.clef or srcObj.getContextByClass('Clef')
     if clefObj is None:
-        clefObj = srcObj.iter.getElementsByClass('Clef').getElementsByOffset(0).first()
+        clefObj = srcObj.getElementsByClass('Clef').getElementsByOffset(0).first()
         # only return clefs that have offset = 0.0
         if not clefObj:
             clefObj = clef.bestClef(srcObj, recurse=True)
@@ -826,8 +826,8 @@ def makeRests(
     else:
         returnObj = s
 
-    if returnObj.iter.parts:
-        for inner_part in returnObj.iter.parts:
+    if returnObj.iter().parts:
+        for inner_part in returnObj.iter().parts:
             inner_part.makeRests(
                 inPlace=True,
                 fillGaps=fillGaps,
@@ -1543,7 +1543,7 @@ def moveNotesToVoices(source, classFilterList=('GeneralNote',)):
     dst = Voice()
 
     # cast to list so source can be edited.
-    affectedElements = list(source.iter.getElementsByClass(classFilterList))
+    affectedElements = list(source.getElementsByClass(classFilterList))
 
     for e in affectedElements:
         dst.insert(source.elementOffset(e), e)
@@ -1653,7 +1653,7 @@ def iterateBeamGroups(
 
     New in v6.7.
     '''
-    iterator: 'music21.stream.iterator.StreamIterator' = s.recurse() if recurse else s.iter
+    iterator: 'music21.stream.iterator.StreamIterator' = s.recurse() if recurse else s.iter()
     current_beam_group: List[note.NotRest] = []
     in_beam_group: bool = False
     for el in iterator.getElementsByClass('NotRest'):
diff --git a/music21/tempo.py b/music21/tempo.py
index 9f0325874..4ad4d502f 100644
--- a/music21/tempo.py
+++ b/music21/tempo.py
@@ -1245,7 +1245,7 @@ def interpolateElements(element1, element2, sourceStream,
 
     scaleAmount = ((endOffsetDest - startOffsetDest) / (endOffsetSrc - startOffsetSrc))
 
-    interpolatedElements = sourceStream.iter.getElementsByOffset(
+    interpolatedElements = sourceStream.getElementsByOffset(
         offsetStart=startOffsetSrc,
         offsetEnd=endOffsetSrc
     )
diff --git a/music21/variant.py b/music21/variant.py
index 7aa3a828d..e3eac69e5 100644
--- a/music21/variant.py
+++ b/music21/variant.py
@@ -153,9 +153,9 @@ def mergeVariants(streamX, streamY, variantName='variant', *, inPlace=False):
     classesX = streamX.classes
     if 'Score' in classesX:
         return mergeVariantScores(streamX, streamY, variantName, inPlace=inPlace)
-    elif streamX.iter.getElementsByClass('Measure'):
+    elif streamX.getElementsByClass('Measure'):
         return mergeVariantMeasureStreams(streamX, streamY, variantName, inPlace=inPlace)
-    elif (streamX.iter.notesAndRests
+    elif (streamX.iter().notesAndRests
             and streamX.duration.quarterLength == streamY.duration.quarterLength):
         return mergeVariantsEqualDuration([streamX, streamY], [variantName], inPlace=inPlace)
     else:
@@ -221,7 +221,7 @@ def mergeVariantScores(aScore, vScore, variantName='variant', *, inPlace=False):
             {3.0} <music21.note.Note F>
             {4.0} <music21.bar.Barline type=final>
     '''
-    if len(aScore.iter.parts) != len(vScore.iter.parts):
+    if len(aScore.iter().parts) != len(vScore.iter().parts):
         raise VariantException(
             'These scores do not have the same number of parts and cannot be merged.')
 
@@ -824,7 +824,7 @@ def mergePartAsOssia(mainPart, ossiaPart, ossiaName,
             if ossiaMeasure.notes:  # If the measure is not just rests
                 ossiaOffset = ossiaMeasure.getOffsetBySite(ossiaPart)
                 if recurseInMeasures is True:
-                    returnMeasure = returnObj.iter.getElementsByOffset(
+                    returnMeasure = returnObj.getElementsByOffset(
                         ossiaOffset
                     ).getElementsByClass(stream.Measure).first()
                     mergeVariantsEqualDuration(
@@ -1952,9 +1952,9 @@ def _getPreviousElement(s, v):
     # Get class of elements in variant or replaced Region
     foundStream = None
     if lengthType == 'elongation':
-        foundStream = v.iter.getElementsByClass(['Measure', 'Note', 'Rest'])
+        foundStream = v.getElementsByClass(['Measure', 'Note', 'Rest'])
     else:
-        foundStream = replacedElements.iter.getElementsByClass(['Measure', 'Note', 'Rest'])
+        foundStream = replacedElements.getElementsByClass(['Measure', 'Note', 'Rest'])
 
     if not foundStream:
         raise VariantException('Cannot find any Measures, Notes, or Rests in variant')
@@ -1964,7 +1964,7 @@ def _getPreviousElement(s, v):
 
     # Get next element in s after v which is of type vClass
     variantOffset = v.getOffsetBySite(s)
-    potentialTargets = s.iter.getElementsByOffset(
+    potentialTargets = s.getElementsByOffset(
         0.0,
         offsetEnd=variantOffset,
         includeEndBoundary=False,
diff --git a/music21/volume.py b/music21/volume.py
index 66e4afdbb..24fc2af7f 100644
--- a/music21/volume.py
+++ b/music21/volume.py
@@ -447,7 +447,7 @@ def realizeVolume(srcStream,
 
     # check for any dynamics
     dynamicsAvailable = False
-    if flatSrc.iter.getElementsByClass('Dynamic'):
+    if flatSrc.getElementsByClass('Dynamic'):
         dynamicsAvailable = True
     else:  # no dynamics available
         if useDynamicContext is True:  # only if True, and non avail, override


Test patch:
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index 9045b9334..62be7ff61 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -2220,8 +2220,8 @@ class Test(unittest.TestCase):
         s.replace(n4, n1)
         self.assertEqual([s[0], s[1]], [n3, n1])
 
-        error_msg = f'{n3} already in {s}'
-        with self.assertRaises(StreamException, msg=error_msg):
+        expected = f'{n3} already in {s}'
+        with self.assertRaisesRegex(StreamException, expected):
             s.replace(n4, n3)
 
     def testReplaceA1(self):
@@ -5344,6 +5344,16 @@ class Test(unittest.TestCase):
         s = o.getScoreByTitle(re.compile('Pfal(.*)'))
         self.assertEqual(s.metadata.title, 'Es fuhr sich ein Pfalzgraf')
 
+    def testOpusSequence(self):
+        '''
+        Providing a sequence of Scores to an Opus container should append
+        rather than insert each at 0.0.
+        '''
+        s1 = Score(Part(Measure(note.Note())))
+        s2 = Score(Part(Measure(note.Note())))
+        o = Opus([s1, s2])
+        self.assertEqual(o.elementOffset(s2), 1.0)
+
     def testActiveSiteMangling(self):
         outer = Stream()
         inner = Stream()
@@ -6116,8 +6126,8 @@ class Test(unittest.TestCase):
 
         # try imported
         s = corpus.parse('bwv66.6')
-        p = s.iter.getElementsByClass('Part').first()  # for test, not .parts, use .iter
-        m = p.iter.getElementsByClass('Measure')[2]  # for test, not .getElementsByClass('Measure')
+        p = s.getElementsByClass('Part').first()  # for test, not .parts, use .iter()
+        m = p.iter().getElementsByClass(Measure)[2]  # for test, not .getElementsByClass('Measure')
         rn = m[2]
 
         self.assertEqual(id(rn.activeSite), id(m))
@@ -8009,7 +8019,7 @@ class Test(unittest.TestCase):
         with self.assertRaises(StreamException):
             Stream([n, n])
         with self.assertRaises(StreamException):
-            Stream([n, None, s.iter])
+            Stream([n, None, s.iter()])
 
     # REMOVED: Turns out that it DOES have fermata on every note!
     # def testSchoenbergChordifyFermatas(self):