Repository: cuthbertLab/music21
Pull request #1295 (2022-05-05 23:56:58)

StreamCore is now a Music21Object; Iterator improvements
Typed Python does not work well with Mixin classes.  There's little harm to making StreamCore a Music21Object, so it is now.

Other changes:
Attempt to unify `__getitem__` on Stream and StreamIterator.  For v8, a bare string w/o a dot or other query mark is discouraged, since in v7 it does different things on Streams (getElement*S*ByClass) and on StreamIterators (getElement*SINGULAR*ById).  In v9, these two types of calls will be unified into the same syntax.


Patch:
diff --git a/.github/workflows/pythonpylint.yml b/.github/workflows/pythonpylint.yml
index a7eb4fae8..ebcf1da97 100644
--- a/.github/workflows/pythonpylint.yml
+++ b/.github/workflows/pythonpylint.yml
@@ -64,5 +64,5 @@ jobs:
             - name: Type-check certain modules with mypy
               run: |
                   mypy --follow-imports=silent music21/capella music21/common music21/corpus music21/features music21/figuredBass music21/humdrum music21/ipython21 music21/languageExcerpts music21/lily music21/mei music21/metadata music21/musedata music21/noteworthy music21/omr music21/romanText music21/test music21/vexflow
-                  mypy --follow-imports=silent music21/stream/base.py music21/stream/core.py music21/stream/enums.py music21/stream/filters.py 
+                  mypy --follow-imports=silent music21/stream/base.py music21/stream/core.py music21/stream/enums.py music21/stream/filters.py music21/stream/iterator.py 
                   mypy --follow-imports=silent music21/articulations.py music21/bar.py music21/base.py music21/beam.py music21/clef.py music21/configure.py music21/defaults.py music21/derivation.py music21/duration.py music21/dynamics.py music21/editorial.py music21/environment.py music21/exceptions21.py music21/expressions.py music21/freezeThaw.py music21/harmony.py music21/instrument.py music21/interval.py music21/key.py music21/layout.py music21/note.py music21/percussion.py music21/pitch.py music21/prebase.py music21/repeat.py music21/roman.py music21/serial.py music21/sieve.py music21/sites.py music21/sorting.py music21/spanner.py music21/style.py music21/tablature.py music21/tempo.py music21/text.py music21/tie.py music21/tinyNotation.py music21/variant.py music21/voiceLeading.py music21/volpiano.py music21/volume.py
diff --git a/music21/base.py b/music21/base.py
index 5475f4ffa..5439d578b 100644
--- a/music21/base.py
+++ b/music21/base.py
@@ -1780,7 +1780,7 @@ class Music21Object(prebase.ProtoM21Object):
         This example shows the context sites for Measure 3 of the
         Alto part. We will get the measure object using direct access to
         indices to ensure that no other temporary
-        streams are created; normally, we would do `c.parts['Alto'].measure(3)`.
+        streams are created; normally, we would do `c.parts['#Alto'].measure(3)`.
 
         >>> m = c[2][4]
         >>> m
@@ -4385,7 +4385,7 @@ class Test(unittest.TestCase):
         from music21 import stream
 
         s = corpus.parse('bach/bwv66.6.xml')
-        p1 = s.parts['Soprano']
+        p1 = s.parts['#Soprano']
 
         # this does not work; cannot get these values from Measures
         #    self.assertEqual(p1.getElementsByClass(stream.Measure)[3].beat, 3)
@@ -4483,7 +4483,7 @@ class Test(unittest.TestCase):
         from music21 import note
 
         s = corpus.parse('bach/bwv66.6.xml')
-        p1 = s.parts['Soprano']
+        p1 = s.parts['#Soprano']
         for classStr in ['Clef', 'KeySignature', 'TimeSignature']:
             self.assertEqual(p1.flatten().getElementsByClass(
                 classStr)[0].measureNumber, 0)
@@ -4594,7 +4594,7 @@ class Test(unittest.TestCase):
         self.maxDiff = None
         s = corpus.parse('bach/bwv103.6')
 
-        p = s.parts['soprano']
+        p = s.parts['#soprano']
         m1 = p.getElementsByClass(stream.Measure).first()
 
         self.assertEqual([n.offset for n in m1.notesAndRests], [0.0, 0.5])
diff --git a/music21/common/decorators.py b/music21/common/decorators.py
index 20796944f..feb464858 100644
--- a/music21/common/decorators.py
+++ b/music21/common/decorators.py
@@ -130,8 +130,17 @@ def deprecated(method, startDate=None, removeDate=None, message=None):
 
     @wraps(method)
     def func_wrapper(*args, **kwargs):
+        if len(args) > 1 and args[1] in ('_ipython_canary_method_should_not_exist_',
+                                         '_repr_mimebundle_'):
+            # false positive from IPython for StreamIterator.__getattr__
+            # can remove after v9.
+            falsePositive = True
+        else:
+
+            falsePositive = False
+
         # TODO: look at sys.warnstatus.
-        if callInfo['calledAlready'] is False:
+        if callInfo['calledAlready'] is False and not falsePositive:
             warnings.warn(callInfo['message'],
                           exceptions21.Music21DeprecationWarning,
                           stacklevel=2)
diff --git a/music21/midi/translate.py b/music21/midi/translate.py
index 8ac484c40..c0cb1885a 100644
--- a/music21/midi/translate.py
+++ b/music21/midi/translate.py
@@ -3143,7 +3143,7 @@ class Test(unittest.TestCase):
         s = corpus.parse('bach/bwv103.6')
 
         # get just the soprano part
-        soprano = s.parts['soprano']
+        soprano = s.parts['#soprano']
         mts = streamHierarchyToMidiTracks(soprano)[1]  # get one
 
         # first note-on is not delayed, even w anacrusis
@@ -3169,7 +3169,7 @@ class Test(unittest.TestCase):
         <music21.midi.DeltaTime (empty) track=1, channel=1>,
         <music21.midi.MidiEvent NOTE_ON, track=1, channel=1, pitch=62, velocity=90>]'''
 
-        alto = s.parts['alto']
+        alto = s.parts['#alto']
         mta = streamHierarchyToMidiTracks(alto)[1]
 
         found = str(mta.events[:8])
@@ -3177,7 +3177,7 @@ class Test(unittest.TestCase):
 
         # try streams to midi tracks
         # get just the soprano part
-        soprano = s.parts['soprano']
+        soprano = s.parts['#soprano']
         mtList = streamHierarchyToMidiTracks(soprano)
         self.assertEqual(len(mtList), 2)
 
diff --git a/music21/prebase.py b/music21/prebase.py
index 8b82153e9..d4f558952 100644
--- a/music21/prebase.py
+++ b/music21/prebase.py
@@ -32,7 +32,7 @@ from music21.common import deprecated
 class ProtoM21Object:
     '''
     A class for pseudo-m21 objects to inherit from.  Any object can inherit from
-    ProtoM21Object and it makes sense for anything a user is likely to encounter
+    ProtoM21Object, and it makes sense for anything a user is likely to encounter
     to inherit from it.  Certain translators, etc. can choose to skip it.
 
     >>> class PitchCounter(prebase.ProtoM21Object):
@@ -168,11 +168,11 @@ class ProtoM21Object:
     @property
     def classSet(self) -> FrozenSet[Union[str, type]]:
         '''
-        Returns a set (that is, unordered, but indexed) of all of the classes that
+        Returns a set (that is, unordered, but indexed) of all classes that
         this class belongs to, including
         string names, fullyQualified string names, and objects themselves.
 
-        It's cached on a per class basis, so makes for a really fast way of checking to
+        It's cached on a per-class basis, so makes for a really fast way of checking to
         see if something belongs
         to a particular class when you don't know if the user has given a string,
         a fully qualified string name, or an object.
@@ -198,12 +198,23 @@ class ProtoM21Object:
         True
 
         >>> sorted([s for s in n.classSet if isinstance(s, str)])
-        ['GeneralNote', 'Music21Object', 'NotRest', 'Note', 'ProtoM21Object',
+        ['GeneralNote',
+         'Music21Object',
+         'NotRest',
+         'Note',
+         'ProtoM21Object',
+         'base.Music21Object',
          'builtins.object',
          'music21.base.Music21Object',
-         'music21.note.GeneralNote', 'music21.note.NotRest', 'music21.note.Note',
+         'music21.note.GeneralNote',
+         'music21.note.NotRest',
+         'music21.note.Note',
          'music21.prebase.ProtoM21Object',
-         'object']
+         'note.GeneralNote',
+         'note.NotRest',
+         'note.Note',
+         'object',
+         'prebase.ProtoM21Object']
 
         >>> sorted([s for s in n.classSet if not isinstance(s, str)], key=lambda x: x.__name__)
         [<class 'music21.note.GeneralNote'>,
@@ -212,14 +223,18 @@ class ProtoM21Object:
          <class 'music21.note.Note'>,
          <class 'music21.prebase.ProtoM21Object'>,
          <class 'object'>]
+
+        changed in v8 -- partially qualified objects such as 'note.Note' have been added.
         '''
         try:
             return self._classSetCacheDict[self.__class__]
         except KeyError:
             classList: List[Union[str, type]] = list(self.classes)
             classList.extend(self.__class__.mro())
-            classList.extend(x.__module__ + '.' + x.__name__ for x in self.__class__.mro())
-
+            fullyQualifiedStrings = [x.__module__ + '.' + x.__name__ for x in self.__class__.mro()]
+            classList.extend(fullyQualifiedStrings)
+            partiallyQualifiedStrings = [x.replace('music21.', '') for x in fullyQualifiedStrings]
+            classList.extend(partiallyQualifiedStrings)
             classSet = frozenset(classList)
             self._classSetCacheDict[self.__class__] = classSet
             return classSet
diff --git a/music21/scale/__init__.py b/music21/scale/__init__.py
index 2457aba57..6fdbf5f1b 100644
--- a/music21/scale/__init__.py
+++ b/music21/scale/__init__.py
@@ -3373,13 +3373,13 @@ class Test(unittest.TestCase):
         sc1 = scale.MajorScale()
         # deriving a new scale from the pitches found in a collection
         s = corpus.parse('bwv66.6')
-        sc3 = sc1.derive(s.parts['soprano'])
+        sc3 = sc1.derive(s.parts['#soprano'])
         self.assertEqual(str(sc3), '<music21.scale.MajorScale A major>')
 
-        sc3 = sc1.derive(s.parts['tenor'])
+        sc3 = sc1.derive(s.parts['#tenor'])
         self.assertEqual(str(sc3), '<music21.scale.MajorScale A major>')
 
-        sc3 = sc2.derive(s.parts['bass'])
+        sc3 = sc2.derive(s.parts['#bass'])
         self.assertEqual(str(sc3), '<music21.scale.MinorScale F# minor>')
 
         # composing with a scale
diff --git a/music21/stream/base.py b/music21/stream/base.py
index 074b34dff..60949a77d 100644
--- a/music21/stream/base.py
+++ b/music21/stream/base.py
@@ -98,9 +98,7 @@ OffsetMap = collections.namedtuple('OffsetMap', ['element', 'offset', 'endTime',
 
 
 # -----------------------------------------------------------------------------
-
-
-class Stream(core.StreamCoreMixin, base.Music21Object, Generic[M21ObjType]):
+class Stream(core.StreamCore, Generic[M21ObjType]):
     '''
     This is the fundamental container for Music21Objects;
     objects may be ordered and/or placed in time based on
@@ -281,8 +279,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object, Generic[M21ObjType]):
                  *args,
                  # restrictClass: Type[M21ObjType] = base.Music21Object,
                  **keywords):
-        base.Music21Object.__init__(self, **keywords)
-        core.StreamCoreMixin.__init__(self)
+        super().__init__(self, *args, **keywords)
 
         self.streamStatus = streamStatus.StreamStatus(self)
         self._unlinkedDuration = None
@@ -527,21 +524,15 @@ class Stream(core.StreamCoreMixin, base.Music21Object, Generic[M21ObjType]):
         >>> c.groups.append('ghost')
         >>> e.groups.append('ghost')
 
-        'Note' is treated as a class name and returns a `RecursiveIterator`:
-
-        >>> for n in s['Note']:
-        ...     print(n.name, end=' ')
-        C C# D E F G A
-
         '.ghost', because it begins with `.`, is treated as a class name and
         returns a `RecursiveIterator`:
 
-
         >>> for n in s['.ghost']:
         ...     print(n.name, end=' ')
         C E
 
-        A query selector with a `#`:
+        A query selector with a `#` returns the single element matching that
+        element or returns None if there is no match:
 
         >>> s['#last_a']
         <music21.note.Note A>
@@ -556,15 +547,21 @@ class Stream(core.StreamCoreMixin, base.Music21Object, Generic[M21ObjType]):
         Traceback (most recent call last):
         TypeError: Streams can get items by int, slice, class, or string query; got <class 'float'>
 
-
         Changed in v7:
-
           - out of range indexes now raise an IndexError, not StreamException
-          - strings ('Note', '#id', '.group') are now treated like a query selector.
+          - strings ('music21.note.Note', '#id', '.group') are now treated like a query selector.
           - slices with negative indices now supported
           - Unsupported types now raise TypeError
           - Class and Group searches now return a recursive `StreamIterator` rather than a `Stream`
           - Slice searches now return a list of elements rather than a `Stream`
+
+        Changed in v8:
+          - for strings: only fully-qualified names such as "music21.note.Note" or
+            partially-qualified names such as "note.Note" are
+            supported as class names.  Better to use a literal type or explicitly call
+            .recurse().getElementsByClass to get the earlier behavior.  Old behavior
+            still works until v9.  This is an attempt to unify __getitem__ behavior in
+            StreamIterators and Streams.
         '''
         # need to sort if not sorted, as this call may rely on index positions
         if not self.isSorted and self.autoSort:
@@ -1585,7 +1582,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object, Generic[M21ObjType]):
             raise ImmutableStreamException('Cannot remove from an immutable stream')
         # TODO: Next to clean up... a doozy -- filter out all the different options.
 
-        # TODO: Add a renumber measures option
+        # TODO: Add an option to renumber measures
         # TODO: Shift offsets if recurse is True
         if shiftOffsets is True and recurse is True:  # pragma: no cover
             raise StreamException(
@@ -4756,7 +4753,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object, Generic[M21ObjType]):
 
 
         >>> chorale = corpus.parse('bach/bwv324.xml')
-        >>> alto = chorale.parts['alto']
+        >>> alto = chorale.parts['#alto']
         >>> altoMeasures = alto.measureOffsetMap()
         >>> altoMeasures
         OrderedDict([(0.0, [<music21.stream.Measure 1 offset=0.0>]),
@@ -7880,7 +7877,7 @@ class Stream(core.StreamCoreMixin, base.Music21Object, Generic[M21ObjType]):
         in quarter lengths. This value usually represents the last
         "release" in the Stream.
 
-        Stream.duration is usually equal to the highestTime
+        Stream.duration is normally equal to the highestTime
         expressed as a Duration object, but it can be set separately
         for advanced operations.
 
@@ -13759,6 +13756,7 @@ class Score(Stream):
         If `inPlace` is True, this is done in-place;
         if `inPlace` is False, this returns a modified deep copy.
         '''
+        returnStream: Score
         if inPlace:
             returnStream = self
         else:
@@ -13784,8 +13782,6 @@ class Score(Stream):
             # no matter, let's just be extra cautious and run this here (Feb 2021 - JTW)
             returnStream.coreElementsChanged()
         else:  # call the base method
-            if TYPE_CHECKING:
-                assert isinstance(returnStream, Score)
             super(Score, returnStream).makeNotation(meterStream=meterStream,
                                                     refStreamOrTimeRange=refStreamOrTimeRange,
                                                     inPlace=True,
@@ -13999,7 +13995,7 @@ class Opus(Stream):
 class SpannerStorage(Stream):
     '''
     For advanced use. This Stream subclass is only used
-    inside of a Spanner object to provide object storage
+    inside a Spanner object to provide object storage
     of connected elements (things the Spanner spans).
 
     This subclass name can be used to search in an
@@ -14059,32 +14055,18 @@ class SpannerStorage(Stream):
 class VariantStorage(Stream):
     '''
     For advanced use. This Stream subclass is only
-    used inside of a Variant object to provide object
+    used inside a Variant object to provide object
     storage of connected elements (things the Variant
     defines).
 
     This subclass name can be used to search in an
     object's .sites and find any and all
-    locations that are VariantStorage objects.
+    locations that are VariantStorage objects.  It also
+    ensures that they are pickled properly as Streams on a non-Stream
+    object.
 
-    A `variantParent` keyword argument must be provided
-    by the Variant in creation.
-
-    # TODO v7: rename variantParent to client
+    Changed in v.8 -- variantParent is removed.  Never used.
     '''
-
-    def __init__(self, *arguments, **keywords):
-        super().__init__(*arguments, **keywords)
-        # must provide a keyword argument with a reference to the variant
-        # parent
-        self.variantParent = None
-        if 'variantParent' in keywords:
-            self.variantParent = keywords['variantParent']
-
-
-# -----------------------------------------------------------------------------
-
-
 # -----------------------------------------------------------------------------
 
 
diff --git a/music21/stream/core.py b/music21/stream/core.py
index 75f038b96..b18e4274f 100644
--- a/music21/stream/core.py
+++ b/music21/stream/core.py
@@ -31,22 +31,20 @@ import unittest
 from music21.base import Music21Object
 from music21.common.enums import OffsetSpecial
 from music21.common.numberTools import opFrac
-from music21.common.types import OffsetQLSpecial
+from music21.common.types import OffsetQLSpecial, M21ObjType
 from music21 import spanner
 from music21 import tree
 from music21.exceptions21 import StreamException, ImmutableStreamException
 from music21.stream.iterator import StreamIterator, RecursiveIterator
 
-if TYPE_CHECKING:
-    from typing import Any
 
-
-# pylint: disable=attribute-defined-outside-init
-class StreamCoreMixin:
+class StreamCore(Music21Object):
     '''
     Core aspects of a Stream's behavior.  Any of these can change at any time.
+    Users are encouraged only to create stream.Stream objects.
     '''
-    def __init__(self):
+    def __init__(self, *arguments, **keywords):
+        super().__init__(*arguments, **keywords)
         # hugely important -- keeps track of where the _elements are
         # the _offsetDict is a dictionary where id(element) is the
         # index and the value is a tuple of offset and element.
@@ -296,11 +294,14 @@ class StreamCoreMixin:
             if keepIndex and indexCache is not None:
                 self._cache['index'] = indexCache
 
-    def coreCopyAsDerivation(self,
+    # core method that has to live in Stream itself for typing purposes.
+    def coreCopyAsDerivation(self: M21ObjType,
                              methodName: str, *,
                              recurse=True,
-                             deep=True) -> 'music21.stream.Stream[Any]':
+                             deep=True) -> M21ObjType:
         '''
+        *This is a Core method that most users will not need to use.*
+
         Make a copy of this stream with the proper derivation set.
 
         >>> s = stream.Stream()
@@ -314,20 +315,15 @@ class StreamCoreMixin:
         >>> s2[0].derivation.method
         'exampleCopy'
         '''
-        if TYPE_CHECKING:
-            from music21 import stream
-            assert isinstance(self, stream.Stream)
+        from music21 import stream
+
         if deep:
             post = copy.deepcopy(self)
         else:  # pragma: no cover
             post = copy.copy(self)
 
-        if TYPE_CHECKING:
-            from music21 import stream
-            assert isinstance(post, stream.Stream)
-
         post.derivation.method = methodName
-        if recurse and deep:
+        if recurse and deep and isinstance(post, stream.Stream):
             post.setDerivationMethod(methodName, recurse=True)
         return post
 
diff --git a/music21/stream/filters.py b/music21/stream/filters.py
index f3b38daab..71aacef22 100644
--- a/music21/stream/filters.py
+++ b/music21/stream/filters.py
@@ -70,9 +70,8 @@ class StreamFilter(prebase.ProtoM21Object):
     # def __init__(self):
     #     pass  # store streamIterator?
 
-    # commented out to make faster, but will be called if exists.
-    # def reset(self):
-    #    pass
+    def reset(self):
+        pass
 
     def __call__(self, item, iterator: Optional[StreamIteratorType] = None):
         return True
diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py
index cd42c0f64..c145f57fc 100644
--- a/music21/stream/iterator.py
+++ b/music21/stream/iterator.py
@@ -20,7 +20,8 @@ from __future__ import annotations
 import copy
 from typing import (TypeVar, List, Union, Callable, Optional, Literal,
                     TypedDict, Generic, overload, Iterable, Type, cast,
-                    Any, TYPE_CHECKING)
+                    Sequence,
+                    Tuple, Any, TYPE_CHECKING)
 import unittest
 import warnings
 
@@ -54,17 +55,16 @@ class StreamIteratorInefficientWarning(PendingDeprecationWarning):
 
 
 class ActiveInformation(TypedDict, total=False):
-    # noinspection PyTypedDict
-    stream: Optional['music21.stream.Stream']  # https://youtrack.jetbrains.com/issue/PY-43689
-    index: int
+    stream: Optional['music21.stream.Stream']
+    elementIndex: int
     iterSection: Literal['_elements', '_endElements']
     sectionIndex: int
-    # noinspection PyTypedDict
-    lastYielded: Optional['music21.stream.Stream']
+    lastYielded: Optional[base.Music21Object]
+
 
 
 # -----------------------------------------------------------------------------
-class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
+class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType], Sequence):
     '''
     An Iterator object used to handle getting items from Streams.
     The :meth:`~music21.stream.Stream.__iter__` method
@@ -79,7 +79,7 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
     Sets:
 
     * StreamIterator.srcStream -- the Stream iterated over
-    * StreamIterator.index -- current index item
+    * StreamIterator.elementIndex -- current index item
     * StreamIterator.streamLength -- length of elements.
 
     * StreamIterator.srcStreamElements -- srcStream._elements
@@ -91,12 +91,12 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
       streams:
 
           * `stream` = the stream that is currently active,
-          * `index` = where in `.elements` we are,
+          * `elementIndex` = where in `.elements` we are,
           * `iterSection` is `_elements` or `_endElements`,
           * `sectionIndex` is where we are in the iterSection, or -1 if
             we have not started.
-          * `lastYielded` the stream that last yielded the element (present in
-            recursiveIterators only).
+          * `lastYielded` the element that was last returned by the iterator.
+            (for OffsetIterators, contains the first element last returned)
           * (This dict is shared among all sub iterators.)
 
     Constructor keyword only arguments:
@@ -113,6 +113,9 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
     * For `activeInformation` see above.
 
     Changed in v.5.2 -- all arguments except srcStream are keyword only.
+    Changed in v.8 -- filterList must be a list or None, not a single filter.
+                      StreamIterator inherits from Sequence, hence index
+                      was moved to elementIndex
 
     OMIT_FROM_DOCS
 
@@ -123,40 +126,40 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
     Traceback (most recent call last):
     TypeError: filterList expects Filters or callables,
     not types themselves; got <class 'music21.note.Note'>
+
+    THIS IS IN OMIT -- Add info above.
     '''
     def __init__(self,
                  srcStream: StreamType,
                  *,
                  # restrictClass: Type[M21ObjType] = base.Music21Object,
-                 filterList: Union[List[FilterType], FilterType, None] = None,
+                 filterList: Optional[List[FilterType]] = None,
                  restoreActiveSites: bool = True,
                  activeInformation: Optional[ActiveInformation] = None,
                  ignoreSorting: bool = False):
         if not ignoreSorting and srcStream.isSorted is False and srcStream.autoSort:
             srcStream.sort()
         self.srcStream: StreamType = srcStream
-        self.index: int = 0
+        self.elementIndex: int = 0
 
         # use .elements instead of ._elements/etc. so that it is sorted...
-        self.srcStreamElements = srcStream.elements
-        self.streamLength = len(self.srcStreamElements)
+        self.srcStreamElements = cast(Tuple[M21ObjType, ...], srcStream.elements)
+        self.streamLength: int = len(self.srcStreamElements)
 
         # this information can help in speed later
-        self.elementsLength = len(self.srcStream._elements)
-        self.sectionIndex = -1  # where we are within a given section (_elements or _endElements)
+        self.elementsLength: int = len(self.srcStream._elements)
+
+        # where we are within a given section (_elements or _endElements)
+        self.sectionIndex: int = -1
         self.iterSection: Literal['_elements', '_endElements'] = '_elements'
 
-        self.cleanupOnStop = False
+        self.cleanupOnStop: bool = False
         self.restoreActiveSites: bool = restoreActiveSites
 
         self.overrideDerivation: Optional[str] = None
 
         if filterList is None:
             filterList = []
-        elif not common.isIterable(filterList):
-            filterList = [filterList]
-        elif isinstance(filterList, (set, tuple)):
-            filterList = list(filterList)  # mutable....
         for x in filterList:
             if isinstance(x, type):
                 raise TypeError(
@@ -165,14 +168,14 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
         # return True or False for an element for
         # whether it should be yielded.
         self.filters: List[FilterType] = filterList
-        self._len = None
-        self._matchingElements = None
+        self._len: Optional[int] = None
+        self._matchingElements: Optional[List[M21ObjType]] = None
         # keep track of where we are in the parse.
         # esp important for recursive streams...
         if activeInformation is not None:
             self.activeInformation: ActiveInformation = activeInformation
         else:
-            self.activeInformation: ActiveInformation = {}
+            self.activeInformation = {}
             self.updateActiveInformation()
 
     def _reprInternal(self):
@@ -184,28 +187,28 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
         if streamClass == 'Measure' and self.srcStream.number != 0:
             srcStreamId = 'm.' + str(self.srcStream.number)
 
-        return f'for {streamClass}:{srcStreamId} @:{self.index}'
+        return f'for {streamClass}:{srcStreamId} @:{self.elementIndex}'
 
     def __iter__(self: _SIter) -> _SIter:
         self.reset()
         return self
 
     def __next__(self) -> M21ObjType:
-        while self.index < self.streamLength:
-            if self.index >= self.elementsLength:
+        while self.elementIndex < self.streamLength:
+            if self.elementIndex >= self.elementsLength:
                 self.iterSection = '_endElements'
-                self.sectionIndex = self.index - self.elementsLength
+                self.sectionIndex = self.elementIndex - self.elementsLength
             else:
-                self.sectionIndex = self.index
+                self.sectionIndex = self.elementIndex
 
             try:
-                e = self.srcStreamElements[self.index]
+                e = self.srcStreamElements[self.elementIndex]
             except IndexError:
                 # this may happen if the number of elements has changed
-                self.index += 1
+                self.elementIndex += 1
                 continue
 
-            self.index += 1
+            self.elementIndex += 1
             if self.matchesFilters(e) is False:
                 continue
 
@@ -213,6 +216,7 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
                 self.srcStream.coreSelfActiveSite(e)
 
             self.updateActiveInformation()
+            self.activeInformation['lastYielded'] = e
             return e
 
         self.cleanup()
@@ -319,10 +323,23 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
         sOut = self.stream()
         return getattr(sOut, attr)
 
+    @overload
     def __getitem__(self, k: int) -> M21ObjType:
+        return self.matchingElements()[k]
+
+    @overload
+    def __getitem__(self, k: slice) -> List[M21ObjType]:
+        return self.matchingElements()
+
+    @overload
+    def __getitem__(self, k: str) -> Optional[M21ObjType]:
+        return None
+
+    def __getitem__(self, k: Union[int, slice, str]) -> Union[M21ObjType,
+                                                              List[M21ObjType],
+                                                              None]:
         '''
-        if you are in the iterator, you should still be able to request other items...
-        uses self.srcStream.__getitem__
+        Iterators can request other items by index or slice.
 
         >>> s = stream.Stream()
         >>> s.insert(0, note.Note('F#'))
@@ -343,6 +360,21 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
         >>> sI.srcStream is s
         True
 
+
+        To request an element by id, put a '#' sign in front of the id,
+        like in HTML DOM queries:
+
+        >>> bach = corpus.parse('bwv66.6')
+        >>> soprano = bach.recurse()['#Soprano']
+        >>> soprano
+        <music21.stream.Part Soprano>
+
+        This behavior is often used to get an element from the Parts iterator:
+
+        >>> bach.parts['#soprano']  # notice: case insensitive
+        <music21.stream.Part Soprano>
+
+
         Slices work:
 
         >>> nSlice = sI[1:]
@@ -368,24 +400,32 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
         ('<music21.note.Note F#>', '<music21.note.Note F#>')
         ('<music21.note.Note C>', '<music21.note.Note F#>')
         ('<music21.note.Note C>', '<music21.note.Note F#>')
-        >>> sI.srcStream is None
-        True
+        >>> sI.srcStream is s  # set to an empty stream
+        False
         >>> for n in sI:
         ...    printer = (repr(n), repr(sI[0]))
         ...    print(printer)
 
         (nothing is printed)
+
+        Changed in v8:
+          - for strings: prepend a '#' sign to get elements by id.
+            The old behavior still works until v9.
+            This is an attempt to unify __getitem__ behavior in
+            StreamIterators and Streams.
         '''
         fe = self.matchingElements()
-        try:
-            e = fe[k]
-        except TypeError:
-            e = None
+        if isinstance(k, str):
+            if k.startswith('#'):
+                # prepare for query selectors.
+                k = k[1:]
+
             for el in fe:
-                if el.id.lower() == k.lower():
-                    e = el
-                    break
-        # TODO: Slices and everything else in Stream __getitem__ ; in fact, merge...
+                if isinstance(el.id, str) and el.id.lower() == k.lower():
+                    return el
+            raise KeyError(k)
+
+        e = fe[k]
         return e
 
     def __len__(self) -> int:
@@ -405,9 +445,10 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
         '''
         if self._len is not None:
             return self._len
-        self._len = len(self.matchingElements(restoreActiveSites=False))
+        lenMatching = len(self.matchingElements(restoreActiveSites=False))
+        self._len = lenMatching
         self.reset()
-        return self._len
+        return lenMatching
 
     def __bool__(self) -> bool:
         '''
@@ -581,26 +622,27 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
     def updateActiveInformation(self) -> None:
         '''
         Updates the (shared) activeInformation dictionary
-        with information about
-        where we are.
+        with information about where we are.
 
-        Call before any element return
+        Call before any element return.
         '''
         ai = self.activeInformation
         ai['stream'] = self.srcStream
-        ai['index'] = self.index - 1
+        ai['elementIndex'] = self.elementIndex - 1
         ai['iterSection'] = self.iterSection
         ai['sectionIndex'] = self.sectionIndex
+        ai['lastYielded'] = None
 
     def reset(self) -> None:
         '''
         reset prior to iteration
         '''
-        self.index = 0
+        self.elementIndex = 0
         self.iterSection = '_elements'
         self.updateActiveInformation()
+        self.activeInformation['lastYielded'] = None
         for f in self.filters:
-            if hasattr(f, 'reset'):
+            if isinstance(f, filters.StreamFilter):
                 f.reset()
 
     def resetCaches(self) -> None:
@@ -617,12 +659,17 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
         '''
         stop iteration; and cleanup if need be.
         '''
-        if self.cleanupOnStop is not False:
+        if self.cleanupOnStop:
             self.reset()
 
+            # cleanupOnStop is rarely used, so we put in
+            # a dummy stream so that srcStream does not need
+            # to be Optional[]
+            SrcStreamClass = self.srcStream.__class__
+
             del self.srcStream
             del self.srcStreamElements
-            self.srcStream = None
+            self.srcStream = SrcStreamClass()
             self.srcStreamElements = ()
 
     # ---------------------------------------------------------------
@@ -683,8 +730,9 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
         if self._matchingElements is not None:
             return self._matchingElements
 
-        with saveAttributes(self, 'restoreActiveSites', 'index'):
+        with saveAttributes(self, 'restoreActiveSites', 'elementIndex'):
             self.restoreActiveSites = restoreActiveSites
+            # we iterate to set all activeSites
             me = [x for x in self]  # pylint: disable=unnecessary-comprehension
             self.reset()
 
@@ -1301,7 +1349,7 @@ class StreamIterator(prebase.ProtoM21Object, Generic[M21ObjType]):
         or includes offset 1.0.
         (Fortunately, this piece begins with a one-beat pickup, so there is such a note):
 
-        >>> soprano = bwv66.parts['Soprano']
+        >>> soprano = bwv66.parts['#Soprano']  # = getElementById('Soprano')
         >>> for el in soprano.recurse().getElementsByOffset(1.0):
         ...     print(el, el.offset, el.getOffsetInHierarchy(bwv66), el.activeSite)
         <music21.stream.Measure 1 offset=1.0> 1.0 1.0 <music21.stream.Part Soprano>
@@ -1533,7 +1581,7 @@ class OffsetIterator(StreamIterator[M21ObjType]):
                 retElOffset = self.srcStream.elementOffset(retEl)
                 retElementList = [retEl]
 
-            while self.index <= self.streamLength:
+            while self.elementIndex <= self.streamLength:
                 nextEl = super().__next__()
                 nextElOffset = self.srcStream.elementOffset(nextEl)
                 if nextElOffset == retElOffset:
@@ -1541,11 +1589,13 @@ class OffsetIterator(StreamIterator[M21ObjType]):
                 else:
                     self.nextToYield = [nextEl]
                     self.nextOffsetToYield = nextElOffset
+                    self.activeInformation['lastYielded'] = retElementList[0]
                     return retElementList
 
         except StopIteration:
             if retElementList:
                 self.raiseStopIterationNext = True
+                self.activeInformation['lastYielded'] = retElementList[0]
                 return retElementList
             else:
                 raise StopIteration
@@ -1698,10 +1748,7 @@ class RecursiveIterator(StreamIterator[M21ObjType]):
                          activeInformation=activeInformation,
                          ignoreSorting=ignoreSorting,
                          )
-        if 'lastYielded' not in self.activeInformation:
-            self.activeInformation['lastYielded'] = None
-
-        self.returnSelf = includeSelf
+        self.returnSelf = includeSelf  # do I still need to return the self object?
         self.includeSelf = includeSelf
         self.ignoreSorting = ignoreSorting
 
@@ -1720,7 +1767,7 @@ class RecursiveIterator(StreamIterator[M21ObjType]):
 
         The same __iter__ as the superclass is used.
         '''
-        while self.index < self.streamLength:
+        while self.elementIndex < self.streamLength:
             # wrap this in a while loop instead of
             # returning self.__next__() because
             # in a long score with a miserly filter
@@ -1735,7 +1782,7 @@ class RecursiveIterator(StreamIterator[M21ObjType]):
 
             if self.returnSelf is True and self.matchesFilters(self.srcStream):
                 self.activeInformation['stream'] = None
-                self.activeInformation['index'] = -1
+                self.activeInformation['elementIndex'] = -1
                 self.activeInformation['lastYielded'] = self.srcStream
                 self.returnSelf = False
                 return cast(M21ObjType, self.srcStream)
@@ -1743,24 +1790,28 @@ class RecursiveIterator(StreamIterator[M21ObjType]):
             elif self.returnSelf is True:
                 self.returnSelf = False
 
-            if self.index >= self.elementsLength:
+            if self.elementIndex >= self.elementsLength:
                 self.iterSection = '_endElements'
-                self.sectionIndex = self.index - self.elementsLength
+                self.sectionIndex = self.elementIndex - self.elementsLength
             else:
-                self.sectionIndex = self.index
+                self.sectionIndex = self.elementIndex
 
             try:
-                e = self.srcStreamElements[self.index]
+                e = self.srcStreamElements[self.elementIndex]
             except IndexError:
-                self.index += 1
+                self.elementIndex += 1
                 # this may happen if the number of elements has changed
                 continue
 
-            self.index += 1
+            self.elementIndex += 1
 
             # in a recursive filter, the stream does not need to match the filter,
             # only the internal elements.
             if e.isStream:
+                if TYPE_CHECKING:
+                    from music21 import stream
+                    assert isinstance(e, stream.Stream)
+
                 childRecursiveIterator: RecursiveIterator[M21ObjType] = RecursiveIterator(
                     srcStream=e,
                     restoreActiveSites=self.restoreActiveSites,
@@ -1803,7 +1854,6 @@ class RecursiveIterator(StreamIterator[M21ObjType]):
         '''
         self.returnSelf = self.includeSelf
         self.childRecursiveIterator = None
-        self.activeInformation['lastYielded'] = None
         super().reset()
 
     def matchingElements(self, *, restoreActiveSites=True):
diff --git a/music21/tree/timespanTree.py b/music21/tree/timespanTree.py
index 324d02a58..2cec1a779 100644
--- a/music21/tree/timespanTree.py
+++ b/music21/tree/timespanTree.py
@@ -90,7 +90,7 @@ class TimespanTree(trees.OffsetTree):
 
     Here in Alto, measure 7, there's a neighbor tone E#.
 
-    >>> bach.parts['Alto'].measure(7).show('text')
+    >>> bach.parts['#Alto'].measure(7).show('text')
     {0.0} <music21.note.Note F#>
     {0.5} <music21.note.Note E#>
     {1.0} <music21.note.Note F#>
@@ -113,7 +113,7 @@ class TimespanTree(trees.OffsetTree):
 
 
     >>> #_DOCS_SHOW newBach = tree.toStream.partwise(scoreTree, templateStream=bach)
-    >>> #_DOCS_SHOW newBach.parts['Alto'].measure(7).show('text')
+    >>> #_DOCS_SHOW newBach.parts['#Alto'].measure(7).show('text')
     {0.0} <music21.chord.Chord F#4>
     {1.5} <music21.chord.Chord F#3>
     {2.0} <music21.chord.Chord C#4>


Test patch:
diff --git a/music21/musicxml/test_m21ToXml.py b/music21/musicxml/test_m21ToXml.py
index 1a62b7be8..f60119835 100644
--- a/music21/musicxml/test_m21ToXml.py
+++ b/music21/musicxml/test_m21ToXml.py
@@ -430,7 +430,7 @@ class Test(unittest.TestCase):
         in the toSoundingPitch and not having their durations restored afterwards
         leading to Instrument objects being split if the duration was complex
         '''
-        alto = corpus.parse('bach/bwv57.8').parts['Alto']
+        alto = corpus.parse('bach/bwv57.8').parts['#Alto']
         alto.measure(7).timeSignature = meter.TimeSignature('6/8')
         newAlto = alto.flat.getElementsNotOfClass(meter.TimeSignature).stream()
         newAlto.insert(0, meter.TimeSignature('2/4'))
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index dca98fa56..8066efae4 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -6106,7 +6106,7 @@ class Test(unittest.TestCase):
 
         # this file was imported by sibelius and does not have completed ties
         sMonte = corpus.parse('monteverdi/madrigal.4.2.xml')
-        s1 = sMonte.parts['Alto']
+        s1 = sMonte.parts['#Alto']
         mStream = s1.getElementsByClass(Measure)
         self.assertEqual([n.offset for n in mStream[3].notesAndRests], [0.0])
         self.assertEqual(str([n.tie for n in mStream[3].notesAndRests]),
@@ -6216,7 +6216,7 @@ class Test(unittest.TestCase):
 
     def testDerivationC(self):
         s = corpus.parse('bach/bwv66.6')
-        p1 = s.parts['Soprano']
+        p1 = s.parts['#Soprano']
         pMeasures = p1.measures(3, 10)
         pMeasuresFlat = pMeasures.flatten()
         pMeasuresFlatNotes = pMeasuresFlat.notesAndRests.stream()