• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cuthbertLab / music21 / 15755105570

19 Jun 2025 09:59AM UTC coverage: 92.99%. Remained the same
15755105570

Pull #1793

github

web-flow
Merge a84caf219 into 4b39c0430
Pull Request #1793: Misc StreamIterator improvements

6 of 9 new or added lines in 1 file covered. (66.67%)

27 existing lines in 1 file now uncovered.

81026 of 87134 relevant lines covered (92.99%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.48
/music21/stream/iterator.py
1
# -*- coding: utf-8 -*-
2
# -----------------------------------------------------------------------------
3
# Name:         stream/iterator.py
4
# Purpose:      Classes for walking through streams
5
#
6
# Authors:      Michael Scott Asato Cuthbert
7
#               Christopher Ariza
8
#
9
# Copyright:    Copyright © 2008-2024 Michael Scott Asato Cuthbert
10
# License:      BSD, see license.txt
11
# -----------------------------------------------------------------------------
12
'''
13
this class contains iterators and filters for walking through streams
14

15
StreamIterators are explicitly allowed to access private methods on streams.
16
'''
17
from __future__ import annotations
1✔
18

19
from collections.abc import Callable, Iterable, Sequence
1✔
20
import copy
1✔
21
import typing as t
1✔
22
from typing import overload  # PyCharm can't use alias
1✔
23
import unittest
1✔
24
import warnings
1✔
25

26
from music21 import common
1✔
27
from music21.common.classTools import tempAttribute, saveAttributes
1✔
28
from music21.common.enums import OffsetSpecial
1✔
29
from music21.common.types import M21ObjType, StreamType, ChangedM21ObjType
1✔
30
from music21 import note
1✔
31
from music21.stream import filters
1✔
32
from music21 import prebase
1✔
33
from music21 import base   # just for typing. (but in a bound, so keep here)
1✔
34

35
from music21.sites import SitesException
1✔
36

37
if t.TYPE_CHECKING:
38
    # need to call this streamModule since we have methods named stream.
39
    from music21 import stream as streamModule
40

41
T = t.TypeVar('T')
1✔
42
S = t.TypeVar('S')
1✔
43
StreamIteratorType = t.TypeVar('StreamIteratorType', bound='StreamIterator')
1✔
44

45
# pipe | version not passing mypy.
46
FilterType = t.Union[Callable[[t.Any, t.Optional[t.Any]], t.Any], filters.StreamFilter]
1✔
47

48

49
# -----------------------------------------------------------------------------
50
class StreamIteratorInefficientWarning(UserWarning):
1✔
51
    pass
1✔
52

53

54
class ActiveInformation(t.TypedDict, total=False):
1✔
55
    stream: streamModule.Stream|None
1✔
56
    elementIndex: int
1✔
57
    iterSection: t.Literal['_elements', '_endElements']
1✔
58
    sectionIndex: int
1✔
59
    lastYielded: base.Music21Object|None
1✔
60

61

62
# -----------------------------------------------------------------------------
63
class StreamIterator(prebase.ProtoM21Object, Sequence[M21ObjType]):
1✔
64
    '''
65
    An Iterator object used to handle getting items from Streams.
66
    The :meth:`~music21.stream.Stream.__iter__` method
67
    returns this object, passing a reference to self.
68

69
    Note that this iterator automatically sets the active site of
70
    returned elements to the source Stream.
71

72
    There is one property to know about: .overrideDerivation which overrides the set
73
    derivation of the class when .stream() is called
74

75
    Sets:
76

77
    * StreamIterator.srcStream -- the Stream iterated over
78
    * StreamIterator.elementIndex -- current index item
79
    * StreamIterator.streamLength -- length of elements.
80

81
    * StreamIterator.srcStreamElements -- srcStream._elements
82
    * StreamIterator.cleanupOnStop -- should the StreamIterator delete the
83
      reference to srcStream and srcStreamElements when stopping? default
84
      False -- DEPRECATED: to be removed in v10.
85
    * StreamIterator.activeInformation -- a dict that contains information
86
      about where we are in the parse.  Especially useful for recursive
87
      streams:
88

89
          * `stream` = the stream that is currently active,
90
          * `elementIndex` = where in `.elements` we are,
91
          * `iterSection` is `_elements` or `_endElements`,
92
          * `sectionIndex` is where we are in the iterSection, or -1 if
93
            we have not started.
94
          * `lastYielded` the element that was last returned by the iterator.
95
            (for OffsetIterators, contains the first element last returned)
96
          * (This dict is shared among all sub iterators.)
97

98
    Constructor keyword-only arguments:
99

100
    * `filterList` is a list of stream.filters.Filter objects to apply
101

102
    * if `restoreActiveSites` is True (default), then on iterating, the activeSite is set
103
      to the Stream being iterated over.
104

105
    * if `ignoreSorting` is True (default is False) then the Stream is not sorted before
106
      iterating.  If the Stream is already sorted, then this value does not matter, and
107
      no time will be saved by setting to False.
108

109
    * For `activeInformation` see above.
110

111
    * Changed in v5.2: all arguments except srcStream are keyword only.
112
    * Changed in v8:
113
      - filterList must be a list or None, not a single filter.
114
      - StreamIterator inherits from typing.Sequence, hence index
115
      was moved to elementIndex.
116
    * Changed in v9: cleanupOnStop is deprecated.  Was not working properly before: noone noticed.
117

118
    OMIT_FROM_DOCS
119

120
    Informative exception for user error:
121

122
    >>> s = stream.Stream()
123
    >>> sIter = stream.iterator.StreamIterator(s, filterList=[note.Note])
124
    Traceback (most recent call last):
125
    TypeError: filterList expects Filters or callables,
126
    not types themselves; got <class 'music21.note.Note'>
127

128
    THIS IS IN OMIT -- Add info above.
129
    '''
130
    def __init__(self,
1✔
131
                 srcStream: StreamType,
132
                 *,
133
                 # restrictClass: type[M21ObjType] = base.Music21Object,
134
                 filterList: list[FilterType]|None = None,
135
                 restoreActiveSites: bool = True,
136
                 activeInformation: ActiveInformation|None = None,
137
                 ignoreSorting: bool = False):
138
        if not ignoreSorting and srcStream.isSorted is False and srcStream.autoSort:
1✔
139
            srcStream.sort()
1✔
140
        self.srcStream: StreamType = srcStream
1✔
141
        self.elementIndex: int = 0
1✔
142

143
        # use .elements instead of ._elements/etc. so that it is sorted
144
        self.srcStreamElements = t.cast(tuple[M21ObjType, ...], srcStream.elements)
1✔
145
        self.streamLength: int = len(self.srcStreamElements)
1✔
146

147
        # this information can help in speed later
148
        # noinspection PyProtectedMember
149
        self.elementsLength: int = len(self.srcStream._elements)
1✔
150

151
        # where we are within a given section (_elements or _endElements)
152
        self.sectionIndex: int = -1
1✔
153
        self.iterSection: t.Literal['_elements', '_endElements'] = '_elements'
1✔
154

155
        self.cleanupOnStop: bool = False
1✔
156
        self.restoreActiveSites: bool = restoreActiveSites
1✔
157

158
        self.overrideDerivation: str|None = None
1✔
159

160
        if filterList is None:
1✔
161
            filterList = []
1✔
162
        for x in filterList:
1✔
163
            if isinstance(x, type):
1✔
164
                raise TypeError(
1✔
165
                    f'filterList expects Filters or callables, not types themselves; got {x}')
166
        # self.filters is a list of expressions that
167
        # return True or False for an element for
168
        # whether it should be yielded.
169
        self.filters: list[FilterType] = filterList
1✔
170
        self._len: int|None = None
1✔
171
        self._matchingElements: dict[bool|None, list[M21ObjType]] = {}
1✔
172
        # keep track of where we are in the parse.
173
        # esp important for recursive streams
174
        if activeInformation is not None:
1✔
175
            self.activeInformation: ActiveInformation = activeInformation
1✔
176
        else:
177
            self.activeInformation = {}
1✔
178
            self.updateActiveInformation()
1✔
179

180
    def _reprInternal(self):
1✔
181
        streamClass = self.srcStream.__class__.__name__
1✔
182
        srcStreamId = self.srcStream.id
1✔
183
        if isinstance(srcStreamId, int):
1✔
184
            srcStreamId = hex(srcStreamId)
1✔
185

186
        if streamClass == 'Measure' and self.srcStream.number != 0:
1✔
187
            srcStreamId = 'm.' + str(self.srcStream.number)
1✔
188

189
        return f'for {streamClass}:{srcStreamId} @:{self.elementIndex}'
1✔
190

191
    def __iter__(self: StreamIteratorType) -> StreamIteratorType:
1✔
192
        self.reset()
1✔
193
        return self
1✔
194

195
    def __next__(self) -> M21ObjType:
1✔
196
        while self.elementIndex < self.streamLength:
1✔
197
            if self.elementIndex >= self.elementsLength:
1✔
198
                self.iterSection = '_endElements'
1✔
199
                self.sectionIndex = self.elementIndex - self.elementsLength
1✔
200
            else:
201
                self.sectionIndex = self.elementIndex
1✔
202

203
            try:
1✔
204
                e = self.srcStreamElements[self.elementIndex]
1✔
205
            except IndexError:
1✔
206
                # this may happen if the number of elements has changed
207
                self.elementIndex += 1
1✔
208
                continue
1✔
209

210
            self.elementIndex += 1
1✔
211
            if self.matchesFilters(e) is False:
1✔
212
                continue
1✔
213

214
            if self.restoreActiveSites is True:
1✔
215
                self.srcStream.coreSelfActiveSite(e)
1✔
216

217
            self.updateActiveInformation()
1✔
218
            self.activeInformation['lastYielded'] = e
1✔
219
            return e
1✔
220

221
        self.cleanup()
1✔
222
        raise StopIteration
1✔
223

224
    def __getattr__(self, attr):
1✔
225
        '''
226
        DEPRECATED in v8 -- will be removed in v9.
227

228
        In case an attribute is defined on Stream but not on a StreamIterator,
229
        create a Stream and then return that attribute.  This is NOT performance
230
        optimized -- calling this repeatedly will mean creating a lot of different
231
        streams.  However, it will prevent most code that worked on v2. from breaking
232
        on v3 and onwards.
233

234
        Deprecated in v8. The upgrade path is to just call `.stream()` on the iterator
235
        before accessing the attribute.
236

237
        >>> s = stream.Measure()
238
        >>> s.insert(0, note.Rest())
239
        >>> s.repeatAppend(note.Note('C'), 2)
240

241
        >>> s.definesExplicitSystemBreaks
242
        False
243

244
        >>> s.notes
245
        <music21.stream.iterator.StreamIterator for Measure:0x101c1a208 @:0>
246

247
        >>> import warnings  #_DOCS_HIDE
248
        >>> with warnings.catch_warnings(): #_DOCS_HIDE
249
        ...      warnings.simplefilter('ignore') #_DOCS_HIDE
250
        ...      explicit = s.notes.definesExplicitSystemBreaks #_DOCS_HIDE
251
        >>> #_DOCS_SHOW explicit = s.notes.definesExplicitSystemBreaks
252
        >>> explicit
253
        False
254

255
        Works with methods as well:
256

257
        >>> with warnings.catch_warnings(): #_DOCS_HIDE
258
        ...      warnings.simplefilter('ignore') #_DOCS_HIDE
259
        ...      popC = s.notes.pop(0) #_DOCS_HIDE
260
        >>> #_DOCS_SHOW popC = s.notes.pop(0)
261
        >>> popC
262
        <music21.note.Note C>
263

264
        But remember that a new Stream is being created each time that an attribute
265
        only defined on a Stream is called, so for instance, so you can pop() forever,
266
        always getting the same element.
267

268
        >>> with warnings.catch_warnings(): #_DOCS_HIDE
269
        ...      warnings.simplefilter('ignore') #_DOCS_HIDE
270
        ...      popC = s.notes.pop(0) #_DOCS_HIDE
271
        >>> #_DOCS_SHOW popC = s.notes.pop(0)
272
        >>> popC
273
        <music21.note.Note C>
274
        >>> with warnings.catch_warnings(): #_DOCS_HIDE
275
        ...      warnings.simplefilter('ignore') #_DOCS_HIDE
276
        ...      popC = s.notes.pop(0) #_DOCS_HIDE
277
        >>> #_DOCS_SHOW popC = s.notes.pop(0)
278
        >>> popC
279
        <music21.note.Note C>
280
        >>> with warnings.catch_warnings(): #_DOCS_HIDE
281
        ...      warnings.simplefilter('ignore') #_DOCS_HIDE
282
        ...      popC = s.notes.pop(0) #_DOCS_HIDE
283
        >>> #_DOCS_SHOW popC = s.notes.pop(0)
284
        >>> popC
285
        <music21.note.Note C>
286

287
        If run with -w, this call will send a StreamIteratorInefficientWarning to stderr
288
        reminding developers that this is not an efficient call, and .stream() should be
289
        called (and probably cached) explicitly.
290

291
        Failures are explicitly given as coming from the StreamIterator object.
292

293
        >>> s.asdf
294
        Traceback (most recent call last):
295
        AttributeError: 'Measure' object has no attribute 'asdf'
296
        >>> s.notes.asdf
297
        Traceback (most recent call last):
298
        AttributeError: 'StreamIterator' object has no attribute 'asdf'
299

300
        OMIT_FROM_DOCS
301

302
        srcStream is accessible, but not with "__getattr__", which joblib uses
303

304
        >>> s.notes.srcStream is s
305
        True
306
        >>> s.notes.__getattr__('srcStream') is None
307
        True
308
        '''
309
        # Prevent infinite loop in feature extractor task serialization
310
        # TODO: investigate if this can be removed once iter becomes iter()
311
        if attr == 'srcStream':
1✔
312
            return None
1✔
313

314
        if not hasattr(self.srcStream, attr):
1✔
315
            # the original stream did not have the attribute, so new won't; but raise on iterator.
316
            raise AttributeError(f'{self.__class__.__name__!r} object has no attribute {attr!r}')
1✔
317

318
        warnings.warn(
1✔
319
            attr + ' is not defined on StreamIterators. Call .stream() first for efficiency',
320
            StreamIteratorInefficientWarning,
321
            stacklevel=2)
322

323
        sOut = self.stream()
1✔
324
        return getattr(sOut, attr)
1✔
325

326
    @overload
1✔
327
    def __getitem__(self, k: int) -> M21ObjType:
1✔
NEW
328
        ...
×
329

330
    @overload
1✔
331
    def __getitem__(self, k: slice) -> list[M21ObjType]:
1✔
NEW
332
        ...
×
333

334
    @overload
1✔
335
    def __getitem__(self, k: str) -> M21ObjType|None:
1✔
NEW
336
        ...
×
337

338
    def __getitem__(self, k: int|slice|str) -> M21ObjType|list[M21ObjType]|None:
1✔
339
        '''
340
        Iterators can request other items by index or slice.
341

342
        >>> s = stream.Stream()
343
        >>> s.insert(0, note.Note('F#'))
344
        >>> s.repeatAppend(note.Note('C'), 2)
345
        >>> sI = s.iter()
346
        >>> sI
347
        <music21.stream.iterator.StreamIterator for Stream:0x104743be0 @:0>
348

349
        >>> sI.srcStream is s
350
        True
351

352
        >>> for n in sI:
353
        ...    printer = (repr(n), repr(sI[0]))
354
        ...    print(printer)
355
        ('<music21.note.Note F#>', '<music21.note.Note F#>')
356
        ('<music21.note.Note C>', '<music21.note.Note F#>')
357
        ('<music21.note.Note C>', '<music21.note.Note F#>')
358
        >>> sI.srcStream is s
359
        True
360

361

362
        To request an element by id, put a '#' sign in front of the id,
363
        like in HTML DOM queries:
364

365
        >>> bach = corpus.parse('bwv66.6')
366
        >>> soprano = bach.recurse()['#Soprano']
367
        >>> soprano
368
        <music21.stream.Part Soprano>
369

370
        This behavior is often used to get an element from the Parts iterator:
371

372
        >>> bach.parts['#soprano']  # notice: case-insensitive retrieval
373
        <music21.stream.Part Soprano>
374

375
        Slices work:
376

377
        >>> nSlice = sI[1:]
378
        >>> for n in nSlice:
379
        ...     print(n)
380
        <music21.note.Note C>
381
        <music21.note.Note C>
382

383
        Filters, such as "notes" apply.
384

385
        >>> s.insert(0, clef.TrebleClef())
386
        >>> s[0]
387
        <music21.clef.TrebleClef>
388
        >>> s.iter().notes[0]
389
        <music21.note.Note F#>
390

391
        Demo of cleanupOnStop = True; the sI[0] call counts as another iteration, so
392
        after it is called, there is nothing more to iterate over!  Note that cleanupOnStop
393
        will be removed in music21 v10.
394

395
        >>> sI.cleanupOnStop = True
396
        >>> for n in sI:
397
        ...    printer = (repr(n), repr(sI[0]))
398
        ...    print(printer)
399
        ('<music21.note.Note F#>', '<music21.note.Note F#>')
400
        >>> sI.srcStream is s  # set to an empty stream
401
        False
402
        >>> for n in sI:
403
        ...    printer = (repr(n), repr(sI[0]))
404
        ...    print(printer)
405

406
        (nothing is printed)
407

408
        * Changed in v8: for strings: prepend a '#' sign to get elements by id.
409
          The old behavior still works until v9.
410
          This is an attempt to unify __getitem__ behavior in
411
          StreamIterators and Streams.
412
        '''
413
        fe = self.matchingElements()
1✔
414
        if isinstance(k, str):
1✔
415
            if k.startswith('#'):
1✔
416
                # prepare for query selectors.
417
                k = k[1:]
1✔
418

419
            for el in fe:
1✔
420
                if isinstance(el.id, str) and el.id.lower() == k.lower():
1✔
421
                    return el
1✔
422
            raise KeyError(k)
×
423

424
        e = fe[k]
1✔
425
        return e
1✔
426

427
    def __len__(self) -> int:
1✔
428
        '''
429
        returns the length of the elements that
430
        match the filter set.
431

432
        >>> s = converter.parse('tinynotation: 3/4 c4 d e f g a', makeNotation=False)
433
        >>> len(s)
434
        7
435
        >>> len(s.iter())
436
        7
437
        >>> len(s.iter().notes)
438
        6
439
        >>> [n.name for n in s.iter().notes]
440
        ['C', 'D', 'E', 'F', 'G', 'A']
441
        '''
442
        if self._len is not None:
1✔
443
            return self._len
1✔
444
        lenMatching = len(self.matchingElements(restoreActiveSites=False))
1✔
445
        self._len = lenMatching
1✔
446
        self.reset()
1✔
447
        return lenMatching
1✔
448

449
    def __bool__(self) -> bool:
1✔
450
        '''
451
        return True if anything matches the filter
452
        otherwise, return False
453

454
        >>> s = converter.parse('tinyNotation: 2/4 c4 r4')
455
        >>> bool(s)
456
        True
457
        >>> iterator = s.recurse()
458
        >>> bool(iterator)
459
        True
460
        >>> bool(iterator.notesAndRests)
461
        True
462
        >>> bool(iterator.notes)
463
        True
464

465
        test cache
466

467
        >>> len(iterator.notes)
468
        1
469
        >>> bool(iterator.notes)
470
        True
471
        >>> bool(iterator.notes)
472
        True
473

474
        >>> iterator = s.recurse()
475
        >>> bool(iterator)
476
        True
477
        >>> bool(iterator)
478
        True
479
        >>> bool(iterator)
480
        True
481

482
        >>> bool(iterator.getElementsByClass(chord.Chord))
483
        False
484

485
        test false cache:
486

487
        >>> len(iterator.getElementsByClass(chord.Chord))
488
        0
489
        >>> bool(iterator.getElementsByClass(chord.Chord))
490
        False
491

492
        '''
493
        if self._len is not None:
1✔
494
            return bool(self._len)
1✔
495

496
        # do not change active site of first element in bool
497
        with tempAttribute(self, 'restoreActiveSites', False):
1✔
498
            for _ in self:
1✔
499
                return True
1✔
500

501
        return False
1✔
502

503
    def __contains__(self, item):
1✔
504
        '''
505
        Does the iterator contain `item`?  Needed for AbstractBaseClass
506
        '''
507
        return item in self.matchingElements(restoreActiveSites=False)
1✔
508

509
    def __reversed__(self):
1✔
510
        me = self.matchingElements()
×
511
        me.reverse()
×
512
        yield from me
×
513

514
    def clone(self: StreamIteratorType) -> StreamIteratorType:
1✔
515
        '''
516
        Returns a new copy of the same iterator.
517
        (a shallow copy of some things except activeInformation)
518
        '''
519
        out: StreamIteratorType = type(self)(
1✔
520
            self.srcStream,
521
            filterList=copy.copy(self.filters),
522
            restoreActiveSites=self.restoreActiveSites,
523
            activeInformation=copy.copy(self.activeInformation),
524
        )
525
        return out
1✔
526

527
    def first(self) -> M21ObjType|None:
1✔
528
        '''
529
        Efficiently return the first matching element, or None if no
530
        elements match.
531

532
        Does not require creating the whole list of matching elements.
533

534
        >>> s = converter.parse('tinyNotation: 3/4 D4 E2 F4 r2 G2 r4')
535
        >>> s.recurse().notes.first()
536
        <music21.note.Note D>
537
        >>> s[note.Rest].first()
538
        <music21.note.Rest half>
539

540
        If no elements match, returns None:
541

542
        >>> print(s[chord.Chord].first())
543
        None
544

545
        * New in v7.
546

547
        OMIT_FROM_DOCS
548

549
        Ensure that next continues after the first note running:
550

551
        >>> notes = s.recurse().notes
552
        >>> notes.first()
553
        <music21.note.Note D>
554
        >>> next(notes)
555
        <music21.note.Note E>
556

557
        Now reset on new iteration:
558

559
        >>> for n in notes:
560
        ...     print(n)
561
        <music21.note.Note D>
562
        <music21.note.Note E>
563
        ...
564

565
        An Empty stream:
566

567
        >>> s = stream.Stream()
568
        >>> s.iter().notes.first() is None
569
        True
570
        '''
571
        iter(self)
1✔
572
        try:
1✔
573
            return next(self)
1✔
574
        except StopIteration:
1✔
575
            return None
1✔
576

577
    def last(self) -> M21ObjType|None:
1✔
578
        '''
579
        Returns the last matching element, or None if no elements match.
580

581
        Currently is not efficient (does not iterate backwards, for instance),
582
        but easier than checking for an IndexError.  Might be refactored later
583
        to iterate the stream backwards instead if it gets a lot of use.
584

585
        >>> s = converter.parse('tinyNotation: 3/4 D4 E2 F4 r2 G2 r4')
586
        >>> s.recurse().notes.last()
587
        <music21.note.Note G>
588
        >>> s[note.Rest].last()
589
        <music21.note.Rest quarter>
590

591
        * New in v7.
592

593
        OMIT_FROM_DOCS
594

595
        Check on empty Stream:
596

597
        >>> s2 = stream.Stream()
598
        >>> s2.iter().notes.last() is None
599
        True
600

601
        Next has a different feature from first(), will start again from beginning.
602
        This behavior may change.
603

604
        >>> notes = s.recurse().notes
605
        >>> notes.last()
606
        <music21.note.Note G>
607
        >>> next(notes)
608
        <music21.note.Note D>
609
        '''
610
        fe = self.matchingElements()
1✔
611
        if not fe:
1✔
612
            return None
1✔
613
        return fe[-1]
1✔
614

615
    # ---------------------------------------------------------------
616
    # start and stop
617
    def updateActiveInformation(self) -> None:
1✔
618
        '''
619
        Updates the (shared) activeInformation dictionary
620
        with information about where we are.
621

622
        Call before any element return.
623
        '''
624
        ai = self.activeInformation
1✔
625
        ai['stream'] = self.srcStream
1✔
626
        ai['elementIndex'] = self.elementIndex - 1
1✔
627
        ai['iterSection'] = self.iterSection
1✔
628
        ai['sectionIndex'] = self.sectionIndex
1✔
629
        ai['lastYielded'] = None
1✔
630

631
    def reset(self) -> None:
1✔
632
        '''
633
        reset prior to iteration
634
        '''
635
        self.elementIndex = 0
1✔
636
        self.iterSection = '_elements'
1✔
637
        self.updateActiveInformation()
1✔
638
        self.activeInformation['lastYielded'] = None
1✔
639
        for f in self.filters:
1✔
640
            if isinstance(f, filters.StreamFilter):
1✔
641
                f.reset()
1✔
642

643
    def resetCaches(self) -> None:
1✔
644
        '''
645
        reset any cached data. -- do not use this at
646
        the start of iteration since we might as well
647
        save this information. But do call it if
648
        the filter changes.
649
        '''
650
        self._len = None
1✔
651
        self._matchingElements = {}
1✔
652

653
    def cleanup(self) -> None:
1✔
654
        '''
655
        stop iteration; and cleanup if need be.
656
        '''
657
        if self.cleanupOnStop:
1✔
658
            self.reset()
1✔
659

660
            # cleanupOnStop is rarely used, so we put in
661
            # a dummy stream so that srcStream does not need
662
            # to be x|None
663
            SrcStreamClass = t.cast(type[StreamType], self.srcStream.__class__)
1✔
664

665
            del self.srcStream
1✔
666
            del self.srcStreamElements
1✔
667
            self.srcStream = SrcStreamClass()
1✔
668
            self.srcStreamElements = ()
1✔
669

670
    # ---------------------------------------------------------------
671
    # getting items
672

673
    def matchingElements(
1✔
674
        self,
675
        *,
676
        restoreActiveSites: bool|None = None
677
    ) -> list[M21ObjType]:
678
        '''
679
        Returns a list of elements that match the filter.
680

681
        This sort of defeats the point of using a generator, so only used if
682
        it's requested by __len__ or __getitem__ etc.
683

684
        Subclasses should override to cache anything they need saved (index,
685
        recursion objects, etc.)
686

687
        activeSite will not be set.
688

689
        Cached for speed.
690

691
        >>> s = converter.parse('tinynotation: 3/4 c4 d e f g a', makeNotation=False)
692
        >>> s.id = 'tn3/4'
693
        >>> sI = s.iter()
694
        >>> sI
695
        <music21.stream.iterator.StreamIterator for Part:tn3/4 @:0>
696

697
        >>> sI.matchingElements()
698
        [<music21.meter.TimeSignature 3/4>, <music21.note.Note C>, <music21.note.Note D>,
699
         <music21.note.Note E>, <music21.note.Note F>, <music21.note.Note G>,
700
         <music21.note.Note A>]
701

702
        >>> sI_notes = sI.notes
703
        >>> sI_notes
704
        <music21.stream.iterator.StreamIterator for Part:tn3/4 @:0>
705

706
        Adding a filter to the Stream iterator returns a new Stream iterator; it
707
        does not change the original.
708

709
        >>> sI_notes is sI
710
        False
711

712
        >>> sI.filters
713
        []
714

715
        >>> sI_notes.filters
716
        [<music21.stream.filters.ClassFilter <class 'music21.note.NotRest'>>]
717

718
        >>> sI_notes.matchingElements()
719
        [<music21.note.Note C>, <music21.note.Note D>,
720
         <music21.note.Note E>, <music21.note.Note F>, <music21.note.Note G>,
721
         <music21.note.Note A>]
722

723
        If restoreActiveSites is False then the elements will not have
724
        their activeSites changed (callers should use it when they do not plan to actually
725
        expose the elements to users, such as in `__len__`).  By default, it is `None`
726
        which means to take from the iterator's `restoreActiveSites` attribute.
727

728
        A demonstration of restoreActiveSites = False.  First we create a second stream
729
        from the first, so that all elements are in two streams, then we'll check the
730
        id of iterating through the second stream normally, and then the first stream
731
        with restoreActiveSites=False, and then the first stream without the restoreActiveSites:
732

733
        >>> s2 = stream.Part()
734
        >>> s2.id = 'second'
735
        >>> s2.elements = s
736
        >>> {e.activeSite.id for e in s2.iter().notes.matchingElements()}
737
        {'second'}
738
        >>> {e.activeSite.id for e in sI_notes.matchingElements(restoreActiveSites=False)}
739
        {'second'}
740
        >>> {e.activeSite.id for e in sI_notes.matchingElements()}
741
        {'tn3/4'}
742

743
        * New in v7: restoreActiveSites
744
        * Changed in v9.3: restoreActiveSites allows `None` which takes from the iterator's
745
          `restoreActiveSites` attribute.
746
        '''
747
        if restoreActiveSites in self._matchingElements:
1✔
748
            return self._matchingElements[restoreActiveSites]
1✔
749

750
        if restoreActiveSites is None:
1✔
751
            restoreActiveSites = self.restoreActiveSites
1✔
752

753
        with saveAttributes(self, 'restoreActiveSites', 'elementIndex'):
1✔
754
            self.restoreActiveSites = restoreActiveSites
1✔
755
            # we iterate to set all activeSites
756
            me = [x for x in self]  # pylint: disable=unnecessary-comprehension
1✔
757
            self.reset()
1✔
758

759
        # cache, by restoreActiveSites parameter
760
        self._matchingElements[restoreActiveSites] = me
1✔
761

762
        return me
1✔
763

764
    def matchesFilters(self, e: base.Music21Object) -> bool:
1✔
765
        '''
766
        returns False if any filter returns False, True otherwise.
767
        '''
768
        f: FilterType
769
        for f in self.filters:
1✔
770
            try:
1✔
771
                try:
1✔
772
                    if f(e, self) is False:
1✔
773
                        return False
1✔
774
                except TypeError:  # one element filters are acceptable.
1✔
775
                    if t.TYPE_CHECKING:
776
                        assert isinstance(f, filters.StreamFilter)
777
                    if f(e) is False:
1✔
UNCOV
778
                        return False
×
779
            except StopIteration:  # pylint: disable=try-except-raise
1✔
780
                raise  # clearer this way to see that this can happen
1✔
781
        return True
1✔
782

783
    def _newBaseStream(self) -> streamModule.Stream:
1✔
784
        '''
785
        Returns a new stream.Stream.  The same thing as calling:
786

787
        >>> s = stream.Stream()
788

789
        This is used in places where returnStreamSubclass is False, so we
790
        cannot just call `type(StreamIterator.srcStream)()`
791

792
        >>> p = stream.Part()
793
        >>> pi = p.iter()
794
        >>> s = pi._newBaseStream()
795
        >>> s
796
        <music21.stream.Stream 0x1047eb2e8>
797
        '''
798
        from music21 import stream
1✔
799
        return stream.Stream()
1✔
800

801
    @overload
1✔
802
    def stream(self, returnStreamSubClass: t.Literal[False]) -> streamModule.Stream:
1✔
UNCOV
803
        ...
×
804

805
    @overload
1✔
806
    def stream(self, returnStreamSubClass: t.Literal[True] = True) -> StreamType:  # type: ignore
1✔
807
        # even after Astroid PR 1015 was fixed, Astroid/mypy since (0.981) reports
808
        # an error (even with this dummy code) saying that it cannot get a StreamType,
809
        #      A function returning TypeVar should receive at least
810
        #      one argument containing the same TypeVar
811
        # But if this were the case then the following method would have the same problem.
UNCOV
812
        x: StreamType = self.streamObj
×
UNCOV
813
        return x
×
814

815

816
    def stream(
1✔
817
        self,
818
        returnStreamSubClass: bool = True
819
    ) -> streamModule.Stream|StreamType:
820
        '''
821
        return a new stream from this iterator.
822

823
        Does nothing except copy if there are no filters, but a drop in
824
        replacement for the old .getElementsByClass() etc. if it does.
825

826
        In other words:
827

828
        `s.getElementsByClass()` == `s.iter().getElementsByClass().stream()`
829

830
        >>> s = stream.Part()
831
        >>> s.insert(0, note.Note('C'))
832
        >>> s.append(note.Rest())
833
        >>> s.append(note.Note('D'))
834
        >>> b = bar.Barline()
835
        >>> s.storeAtEnd(b)
836

837
        >>> s2 = s.iter().getElementsByClass(note.Note).stream()
838
        >>> s2.show('t')
839
        {0.0} <music21.note.Note C>
840
        {2.0} <music21.note.Note D>
841
        >>> s2.derivation.method
842
        'getElementsByClass'
843
        >>> s2
844
        <music21.stream.Part ...>
845

846
        >>> s3 = s.iter().stream()
847
        >>> s3.show('t')
848
        {0.0} <music21.note.Note C>
849
        {1.0} <music21.note.Rest quarter>
850
        {2.0} <music21.note.Note D>
851
        {3.0} <music21.bar.Barline type=regular>
852

853
        >>> s3.elementOffset(b, returnSpecial=True)
854
        <OffsetSpecial.AT_END>
855

856
        >>> s4 = s.iter().getElementsByClass(bar.Barline).stream()
857
        >>> s4.show('t')
858
        {0.0} <music21.bar.Barline type=regular>
859

860

861
        Note that this routine can create Streams that have elements that the original
862
        stream did not, in the case of recursion:
863

864
        >>> bach = corpus.parse('bwv66.6')
865
        >>> bn = bach.flatten()[34]
866
        >>> bn
867
        <music21.note.Note E>
868

869
        >>> bn in bach
870
        False
871
        >>> bfn = bach.recurse().notes.stream()
872
        >>> bn in bfn
873
        True
874
        >>> bn.getOffsetBySite(bfn)
875
        2.0
876
        >>> bn.getOffsetInHierarchy(bach)
877
        2.0
878

879
        OMIT_FROM_DOCS
880

881
        >>> s4._endElements[0] is b
882
        True
883
        '''
884
        ss = self.srcStream
1✔
885

886
        # if this stream was sorted, the resultant stream is sorted
887
        clearIsSorted = False
1✔
888
        found: streamModule.Stream|StreamType
889
        if returnStreamSubClass:
1✔
890
            try:
1✔
891
                # PyCharm??? totally used!
892
                # noinspection PyUnusedLocal
893
                found = ss.__class__()
1✔
894
            except TypeError:
×
895
                found = self._newBaseStream()
×
896
        else:
897
            found = self._newBaseStream()
1✔
898

899
        found.mergeAttributes(ss)
1✔
900
        found.derivation.origin = ss
1✔
901
        if self.overrideDerivation is not None:
1✔
902
            found.derivation.method = self.overrideDerivation
1✔
903
        else:
904
            derivationMethods = []
1✔
905
            for f in self.filters:
1✔
906
                if isinstance(f, filters.StreamFilter):
1✔
907
                    dStr = f.derivationStr
1✔
908
                else:
909
                    dStr = f.__name__  # function; lambda returns <lambda>
1✔
910
                derivationMethods.append(dStr)
1✔
911
            found.derivation.method = '.'.join(derivationMethods)
1✔
912

913
        fe = self.matchingElements()
1✔
914
        for e in fe:
1✔
915
            try:
1✔
916
                o = ss.elementOffset(e, returnSpecial=True)
1✔
917
            except SitesException:
1✔
918
                # this can happen in the case of, s.recurse().notes.stream() -- need to do new
919
                # stream
920
                o = e.getOffsetInHierarchy(ss)
1✔
921
                clearIsSorted = True  # now the stream is probably not sorted
1✔
922

923
            if not isinstance(o, str):
1✔
924
                found.coreInsert(o, e, ignoreSort=True)
1✔
925
            else:
926
                if o == OffsetSpecial.AT_END:
1✔
927
                    found.coreStoreAtEnd(e)
1✔
928
                else:
929
                    # TODO: something different
UNCOV
930
                    found.coreStoreAtEnd(e)
×
931

932
        if fe:
1✔
933
            found.coreElementsChanged(clearIsSorted=clearIsSorted)
1✔
934

935
        return found
1✔
936

937
    @property
1✔
938
    def activeElementList(self) -> t.Literal['_elements', '_endElements']:
1✔
939
        '''
940
        Returns the element list (`_elements` or `_endElements`)
941
        for the current activeInformation.
942
        '''
UNCOV
943
        return getattr(self.activeInformation['stream'], self.activeInformation['iterSection'])
×
944

945
    # ------------------------------------------------------------
946

947
    def addFilter(
1✔
948
        self: StreamIteratorType,
949
        newFilter,
950
        *,
951
        returnClone=True
952
    ) -> StreamIteratorType:
953
        '''
954
        Return a new StreamIterator with an additional filter.
955
        Also resets caches -- so do not add filters any other way.
956

957
        If returnClone is False then adds without creating a new StreamIterator
958

959
        * Changed in v6: Encourage creating new StreamIterators: change
960
          default to return a new StreamIterator.
961
        '''
962
        if returnClone:
1✔
963
            out = self.clone()
1✔
964
        else:
965
            out = self
1✔
966

967
        out.resetCaches()
1✔
968
        for f in out.filters:
1✔
969
            if newFilter == f:
1✔
970
                return out
1✔
971
        out.filters.append(newFilter)
1✔
972

973
        return out
1✔
974

975
    def removeFilter(
1✔
976
        self: StreamIteratorType,
977
        oldFilter,
978
        *,
979
        returnClone=True
980
    ) -> StreamIteratorType:
981
        '''
982
        Return a new StreamIterator where oldFilter is removed.
983
        '''
UNCOV
984
        if returnClone:
×
UNCOV
985
            out = self.clone()
×
986
        else:
987
            out = self
×
988

UNCOV
989
        out.resetCaches()
×
990
        if oldFilter in out.filters:
×
UNCOV
991
            out.filters.pop(out.filters.index(oldFilter))
×
992

993
        return out
×
994

995
    def getElementById(self, elementId: str) -> M21ObjType|None:
1✔
996
        '''
997
        Returns a single element (or None) that matches elementId.
998

999
        If chaining filters, this should be the last one, as it returns an element
1000

1001
        >>> s = stream.Stream(id='s1')
1002
        >>> s.append(note.Note('C'))
1003
        >>> r = note.Rest()
1004
        >>> r.id = 'restId'
1005
        >>> s.append(r)
1006
        >>> r2 = s.recurse().getElementById('restId')
1007
        >>> r2 is r
1008
        True
1009
        >>> r2.id
1010
        'restId'
1011
        '''
1012
        out = self.addFilter(filters.IdFilter(elementId))
1✔
1013
        for e in out:
1✔
1014
            return e
1✔
1015
        return None
1✔
1016

1017
    @overload
1✔
1018
    def getElementsByClass(self,
1✔
1019
                           classFilterList: str,
1020
                           *,
1021
                           returnClone: bool = True) -> StreamIterator[M21ObjType]:
UNCOV
1022
        ...
×
1023

1024
    @overload
1✔
1025
    def getElementsByClass(self,
1✔
1026
                           classFilterList: Iterable[str],
1027
                           *,
1028
                           returnClone: bool = True) -> StreamIterator[M21ObjType]:
UNCOV
1029
        ...
×
1030

1031
    # @overload
1032
    # def getElementsByClass(self,
1033
    #                        classFilterList: type,
1034
    #                        *,
1035
    #                        returnClone: bool = True) -> StreamIterator[M21ObjType]:
1036
    #   ...
1037

1038
    @overload
1✔
1039
    def getElementsByClass(self,
1✔
1040
                           classFilterList: type[ChangedM21ObjType],
1041
                           *,
1042
                           returnClone: bool = True) -> StreamIterator[ChangedM21ObjType]:
UNCOV
1043
        ...
×
1044

1045
    @overload
1✔
1046
    def getElementsByClass(self,
1✔
1047
                           classFilterList: Iterable[type],
1048
                           *,
1049
                           returnClone: bool = True) -> StreamIterator[M21ObjType]:
UNCOV
1050
        ...
×
1051

1052
    def getElementsByClass(
1✔
1053
        self,
1054
        classFilterList: t.Union[
1055
            str,
1056
            type[ChangedM21ObjType],
1057
            Iterable[str],
1058
            Iterable[type],
1059
        ],
1060
        *,
1061
        returnClone: bool = True
1062
    ) -> t.Union[StreamIterator[M21ObjType], StreamIterator[ChangedM21ObjType]]:
1063
        '''
1064
        Add a filter to the Iterator to remove all elements
1065
        except those that match one
1066
        or more classes in the `classFilterList`. A single class
1067
        can also be used for the `classFilterList` parameter instead of a List.
1068

1069
        >>> s = stream.Stream(id='s1')
1070
        >>> s.append(note.Note('C'))
1071
        >>> r = note.Rest()
1072
        >>> s.append(r)
1073
        >>> s.append(note.Note('D'))
1074
        >>> for el in s.iter().getElementsByClass(note.Rest):
1075
        ...     print(el)
1076
        <music21.note.Rest quarter>
1077

1078

1079
        ActiveSite is restored.
1080

1081
        >>> s2 = stream.Stream(id='s2')
1082
        >>> s2.insert(0, r)
1083
        >>> r.activeSite.id
1084
        's2'
1085

1086
        >>> for el in s.iter().getElementsByClass(note.Rest):
1087
        ...     print(el.activeSite.id)
1088
        s1
1089

1090

1091
        Strings work in addition to classes, but your IDE will not know that
1092
        `el` is a :class:`~music21.note.Rest` object.
1093

1094
        >>> for el in s.iter().getElementsByClass('Rest'):
1095
        ...     print(el)
1096
        <music21.note.Rest quarter>
1097
        '''
1098
        return self.addFilter(filters.ClassFilter(classFilterList), returnClone=returnClone)
1✔
1099

1100
    def getElementsByQuerySelector(self, querySelector: str, *, returnClone=True):
1✔
1101
        '''
1102
        First implementation of a query selector, similar to CSS QuerySelectors used in
1103
        HTML DOM:
1104

1105
        * A leading `#` indicates the id of an element, so '#hello' will find elements
1106
          with `el.id=='hello'` (should only be one)
1107
        * A leading `.` indicates the group of an element, so '.high' will find elements
1108
          with `'high'` in el.groups.
1109
        * Any other string is considered to be the type/class of the element.  So `Note`
1110
          will find all Note elements.  Can be fully qualified like `music21.note.Note`
1111
          or partially qualified like `note.Note`.
1112

1113
        Eventually, more complex query selectors will be implemented.  This is just a start.
1114

1115
        Setting up an example:
1116

1117
        >>> s = converter.parse('tinyNotation: 4/4 GG4 AA4 BB4 r4 C4 D4 E4 F4 r1')
1118
        >>> s[note.Note].last().id = 'last'
1119
        >>> for n in s[note.Note]:
1120
        ...     if n.octave == 3:
1121
        ...         n.groups.append('tenor')
1122

1123
        >>> list(s.recurse().getElementsByQuerySelector('.tenor'))
1124
        [<music21.note.Note C>,
1125
         <music21.note.Note D>,
1126
         <music21.note.Note E>,
1127
         <music21.note.Note F>]
1128

1129
        >>> list(s.recurse().getElementsByQuerySelector('Rest'))
1130
        [<music21.note.Rest quarter>,
1131
         <music21.note.Rest whole>]
1132

1133
        Note that unlike with stream slices, the querySelector does not do anything special
1134
        for id searches.  `.first()` will need to be called to find the element (if any)
1135

1136
        >>> s.recurse().getElementsByQuerySelector('#last').first()
1137
        <music21.note.Note F>
1138

1139
        * New in v7.
1140
        '''
1141
        if querySelector.startswith('#'):
1✔
1142
            return self.addFilter(filters.IdFilter(querySelector[1:]), returnClone=returnClone)
1✔
1143
        if querySelector.startswith('.'):
1✔
1144
            return self.addFilter(filters.GroupFilter(querySelector[1:]), returnClone=returnClone)
1✔
1145
        return self.addFilter(filters.ClassFilter(querySelector), returnClone=returnClone)
1✔
1146

1147

1148
    def getElementsNotOfClass(self, classFilterList, *, returnClone=True):
1✔
1149
        '''
1150
        Adds a filter, removing all Elements that do not
1151
        match the one or more classes in the `classFilterList`.
1152

1153
        In lieu of a list, a single class can be used as the `classFilterList` parameter.
1154

1155
        >>> a = stream.Stream()
1156
        >>> a.repeatInsert(note.Rest(), range(10))
1157
        >>> for x in range(4):
1158
        ...     n = note.Note('G#')
1159
        ...     n.offset = x * 3
1160
        ...     a.insert(n)
1161
        >>> found = a.iter().getElementsNotOfClass(note.Note)
1162
        >>> len(found)
1163
        10
1164
        >>> found = a.iter().getElementsNotOfClass('Rest')
1165
        >>> len(found)
1166
        4
1167
        >>> found = a.iter().getElementsNotOfClass(['Note', 'Rest'])
1168
        >>> len(found)
1169
        0
1170

1171
        >>> b = stream.Stream()
1172
        >>> b.repeatInsert(note.Rest(), range(15))
1173
        >>> a.insert(b)
1174

1175
        >>> found = a.recurse().getElementsNotOfClass([note.Rest, 'Stream'])
1176
        >>> len(found)
1177
        4
1178
        >>> found = a.recurse().getElementsNotOfClass([note.Note, 'Stream'])
1179
        >>> len(found)
1180
        25
1181
        '''
1182
        return self.addFilter(filters.ClassNotFilter(classFilterList), returnClone=returnClone)
1✔
1183

1184
    def getElementsByGroup(self, groupFilterList, *, returnClone=True):
1✔
1185
        '''
1186
        >>> n1 = note.Note('C')
1187
        >>> n1.groups.append('trombone')
1188
        >>> n2 = note.Note('D')
1189
        >>> n2.groups.append('trombone')
1190
        >>> n2.groups.append('tuba')
1191
        >>> n3 = note.Note('E')
1192
        >>> n3.groups.append('tuba')
1193
        >>> s1 = stream.Stream()
1194
        >>> s1.append(n1)
1195
        >>> s1.append(n2)
1196
        >>> s1.append(n3)
1197

1198
        >>> tboneSubStream = s1.iter().getElementsByGroup('trombone')
1199
        >>> for thisNote in tboneSubStream:
1200
        ...     print(thisNote.name)
1201
        C
1202
        D
1203
        >>> tubaSubStream = s1.iter().getElementsByGroup('tuba')
1204
        >>> for thisNote in tubaSubStream:
1205
        ...     print(thisNote.name)
1206
        D
1207
        E
1208
        '''
1209
        return self.addFilter(filters.GroupFilter(groupFilterList), returnClone=returnClone)
1✔
1210

1211
    def getElementsByOffset(
1✔
1212
        self: StreamIteratorType,
1213
        offsetStart,
1214
        offsetEnd=None,
1215
        *,
1216
        includeEndBoundary=True,
1217
        mustFinishInSpan=False,
1218
        mustBeginInSpan=True,
1219
        includeElementsThatEndAtStart=True,
1220
        stopAfterEnd=True,
1221
        returnClone=True,
1222
    ) -> StreamIteratorType:
1223
        '''
1224
        Adds a filter keeping only Music21Objects that
1225
        are found at a certain offset or within a certain
1226
        offset time range (given the start and optional stop values).
1227

1228
        There are several attributes that govern how this range is
1229
        determined:
1230

1231

1232
        If `mustFinishInSpan` is True then an event that begins
1233
        between offsetStart and offsetEnd but which ends after offsetEnd
1234
        will not be included.  The default is False.
1235

1236

1237
        For instance, a half note at offset 2.0 will be found in
1238
        getElementsByOffset(1.5, 2.5) or getElementsByOffset(1.5, 2.5,
1239
        mustFinishInSpan = False) but not by getElementsByOffset(1.5, 2.5,
1240
        mustFinishInSpan = True).
1241

1242
        The `includeEndBoundary` option determines if an element
1243
        begun just at the offsetEnd should be included.  For instance,
1244
        the half note at offset 2.0 above would be found by
1245
        getElementsByOffset(0, 2.0) or by getElementsByOffset(0, 2.0,
1246
        includeEndBoundary = True) but not by getElementsByOffset(0, 2.0,
1247
        includeEndBoundary = False).
1248

1249
        Setting includeEndBoundary to False at the same time as
1250
        mustFinishInSpan is set to True is probably NOT what you want to do
1251
        unless you want to find things like clefs at the end of the region
1252
        to display as courtesy clefs.
1253

1254
        The `mustBeginInSpan` option determines whether notes or other
1255
        objects that do not begin in the region but are still sounding
1256
        at the beginning of the region are excluded.  The default is
1257
        True -- that is, these notes will not be included.
1258
        For instance the half note at offset 2.0 from above would not be found by
1259
        getElementsByOffset(3.0, 3.5) or getElementsByOffset(3.0, 3.5,
1260
        mustBeginInSpan = True) but it would be found by
1261
        getElementsByOffset(3.0, 3.5, mustBeginInSpan = False)
1262

1263
        Setting includeElementsThatEndAtStart to False is useful for zeroLength
1264
        searches that set mustBeginInSpan == False to not catch notes that were
1265
        playing before the search but that end just before the end of the search type.
1266
        See the code for allPlayingWhileSounding for a demonstration.
1267

1268
        This chart, like the examples below, demonstrates the various
1269
        features of getElementsByOffset.  It is one of the most complex
1270
        methods of music21 but also one of the most powerful, so it
1271
        is worth learning at least the basics.
1272

1273
            .. image:: images/getElementsByOffset.*
1274
                :width: 600
1275

1276
        >>> st1 = stream.Stream()
1277
        >>> n0 = note.Note('C')
1278
        >>> n0.duration.type = 'half'
1279
        >>> n0.offset = 0
1280
        >>> st1.insert(n0)
1281
        >>> n2 = note.Note('D')
1282
        >>> n2.duration.type = 'half'
1283
        >>> n2.offset = 2
1284
        >>> st1.insert(n2)
1285
        >>> out1 = list(st1.iter().getElementsByOffset(2))
1286
        >>> len(out1)
1287
        1
1288
        >>> out1[0].step
1289
        'D'
1290
        >>> out2 = list(st1.iter().getElementsByOffset(1, 3))
1291
        >>> len(out2)
1292
        1
1293
        >>> out2[0].step
1294
        'D'
1295
        >>> out3 = list(st1.iter().getElementsByOffset(1, 3, mustFinishInSpan=True))
1296
        >>> len(out3)
1297
        0
1298
        >>> out4 = list(st1.iter().getElementsByOffset(1, 2))
1299
        >>> len(out4)
1300
        1
1301
        >>> out4[0].step
1302
        'D'
1303
        >>> out5 = list(st1.iter().getElementsByOffset(1, 2, includeEndBoundary=False))
1304
        >>> len(out5)
1305
        0
1306
        >>> out6 = list(st1.iter().getElementsByOffset(1, 2, includeEndBoundary=False,
1307
        ...                                          mustBeginInSpan=False))
1308
        >>> len(out6)
1309
        1
1310
        >>> out6[0].step
1311
        'C'
1312
        >>> out7 = list(st1.iter().getElementsByOffset(1, 3, mustBeginInSpan=False))
1313
        >>> len(out7)
1314
        2
1315
        >>> [el.step for el in out7]
1316
        ['C', 'D']
1317

1318
        Note, that elements that end at the start offset are included if mustBeginInSpan is False
1319

1320
        >>> out8 = list(st1.iter().getElementsByOffset(2, 4, mustBeginInSpan=False))
1321
        >>> len(out8)
1322
        2
1323
        >>> [el.step for el in out8]
1324
        ['C', 'D']
1325

1326
        To change this behavior set includeElementsThatEndAtStart=False
1327

1328
        >>> out9 = list(st1.iter().getElementsByOffset(2, 4, mustBeginInSpan=False,
1329
        ...                                          includeElementsThatEndAtStart=False))
1330
        >>> len(out9)
1331
        1
1332
        >>> [el.step for el in out9]
1333
        ['D']
1334

1335
        >>> a = stream.Stream(id='a')
1336
        >>> n = note.Note('G')
1337
        >>> n.quarterLength = 0.5
1338
        >>> a.repeatInsert(n, list(range(8)))
1339
        >>> b = stream.Stream(id='b')
1340
        >>> b.repeatInsert(a, [0, 3, 6])
1341
        >>> c = list(b.iter().getElementsByOffset(2, 6.9))
1342
        >>> len(c)
1343
        2
1344
        >>> c = list(b.flatten().iter().getElementsByOffset(2, 6.9))
1345
        >>> len(c)
1346
        10
1347

1348
        Testing multiple zero-length elements with mustBeginInSpan:
1349

1350
        >>> c = clef.TrebleClef()
1351
        >>> ts = meter.TimeSignature('4/4')
1352
        >>> ks = key.KeySignature(2)
1353
        >>> s = stream.Stream()
1354
        >>> s.insert(0.0, c)
1355
        >>> s.insert(0.0, ts)
1356
        >>> s.insert(0.0, ks)
1357
        >>> len(list(s.iter().getElementsByOffset(0.0, mustBeginInSpan=True)))
1358
        3
1359
        >>> len(list(s.iter().getElementsByOffset(0.0, mustBeginInSpan=False)))
1360
        3
1361

1362
        On a :class:`~music21.stream.iterator.RecursiveIterator`,
1363
        `.getElementsByOffset(0.0)`, will get everything
1364
        at the start of the piece, which is useful:
1365

1366
        >>> bwv66 = corpus.parse('bwv66.6')
1367
        >>> list(bwv66.recurse().getElementsByOffset(0.0))
1368
        [<music21.metadata.Metadata object at 0x10a32f490>,
1369
         <music21.stream.Part Soprano>,
1370
         <music21.instrument.Instrument 'P1: Soprano: Instrument 1'>,
1371
         <music21.stream.Measure 0 offset=0.0>,
1372
         <music21.clef.TrebleClef>,
1373
         <music21.tempo.MetronomeMark Quarter=96 (playback only)>,
1374
         <music21.key.Key of f# minor>,
1375
         <music21.meter.TimeSignature 4/4>,
1376
         <music21.note.Note C#>,
1377
         <music21.stream.Part Alto>,
1378
         ...
1379
         <music21.note.Note E>,
1380
         <music21.stream.Part Tenor>,
1381
         ...]
1382

1383
        However, any other offset passed to `getElementsByOffset` on a
1384
        `RecursiveIterator` without additional arguments, is unlikely to be useful,
1385
        because the iterator ends as soon as it encounters an element
1386
        with an offset beyond the `offsetEnd` point.  For instance,
1387
        calling `.getElementsByOffset(1.0).notes` on a :class:`~music21.stream.Part`,
1388
        in bwv66.6 only gets the note that appears at offset 1.0 of a measure that begins
1389
        or includes offset 1.0.
1390
        (Fortunately, this piece begins with a one-beat pickup, so there is such a note):
1391

1392
        >>> soprano = bwv66.parts['#Soprano']  # = getElementById('Soprano')
1393
        >>> for el in soprano.recurse().getElementsByOffset(1.0):
1394
        ...     print(el, el.offset, el.getOffsetInHierarchy(bwv66), el.activeSite)
1395
        <music21.stream.Measure 1 offset=1.0> 1.0 1.0 <music21.stream.Part Soprano>
1396
        <music21.note.Note B> 1.0 2.0 <music21.stream.Measure 1 offset=1.0>
1397

1398

1399
        RecursiveIterators will probably want to use
1400
        :meth:`~music21.stream.iterator.RecursiveIterator.getElementsByOffsetInHierarchy`
1401
        instead.  Or to get all elements with a particular local offset, such as everything
1402
        on the third quarter note of a measure, use the `stopAfterEnd=False` keyword,
1403
        which lets the iteration continue to search for elements even after encountering
1404
        some within Streams whose offsets are greater than the end element.
1405

1406
        >>> len(soprano.recurse().getElementsByOffset(2.0, stopAfterEnd=False))
1407
        9
1408

1409
        * Changed in v5.5: all arguments changing behavior are keyword only.
1410
        * New in v6.5: `stopAfterEnd` keyword.
1411

1412
        OMIT_FROM_DOCS
1413

1414
        Same test as above, but with floats
1415

1416
        >>> out1 = list(st1.iter().getElementsByOffset(2.0))
1417
        >>> len(out1)
1418
        1
1419
        >>> out1[0].step
1420
        'D'
1421
        >>> out2 = list(st1.iter().getElementsByOffset(1.0, 3.0))
1422
        >>> len(out2)
1423
        1
1424
        >>> out2[0].step
1425
        'D'
1426
        >>> out3 = list(st1.iter().getElementsByOffset(1.0, 3.0, mustFinishInSpan=True))
1427
        >>> len(out3)
1428
        0
1429
        >>> out3b = list(st1.iter().getElementsByOffset(0.0, 3.001, mustFinishInSpan=True))
1430
        >>> len(out3b)
1431
        1
1432
        >>> out3b[0].step
1433
        'C'
1434
        >>> out3b = list(st1.iter().getElementsByOffset(1.0, 3.001, mustFinishInSpan=True,
1435
        ...                                           mustBeginInSpan=False))
1436
        >>> len(out3b)
1437
        1
1438
        >>> out3b[0].step
1439
        'C'
1440

1441
        >>> out4 = list(st1.iter().getElementsByOffset(1.0, 2.0))
1442
        >>> len(out4)
1443
        1
1444
        >>> out4[0].step
1445
        'D'
1446
        >>> out5 = list(st1.iter().getElementsByOffset(1.0, 2.0, includeEndBoundary=False))
1447
        >>> len(out5)
1448
        0
1449
        >>> out6 = list(st1.iter().getElementsByOffset(1.0, 2.0, includeEndBoundary=False,
1450
        ...                                          mustBeginInSpan=False))
1451
        >>> len(out6)
1452
        1
1453
        >>> out6[0].step
1454
        'C'
1455
        >>> out7 = list(st1.iter().getElementsByOffset(1.0, 3.0, mustBeginInSpan=False))
1456
        >>> len(out7)
1457
        2
1458
        >>> [el.step for el in out7]
1459
        ['C', 'D']
1460
        '''
1461
        return self.addFilter(
1✔
1462
            filters.OffsetFilter(
1463
                offsetStart,
1464
                offsetEnd,
1465
                includeEndBoundary=includeEndBoundary,
1466
                mustFinishInSpan=mustFinishInSpan,
1467
                mustBeginInSpan=mustBeginInSpan,
1468
                includeElementsThatEndAtStart=includeElementsThatEndAtStart,
1469
                stopAfterEnd=stopAfterEnd,
1470
            ),
1471
            returnClone=returnClone
1472
        )
1473

1474
    # ------------------------------------------------------------
1475
    # properties -- historical
1476

1477
    @property
1✔
1478
    def notes(self):
1✔
1479
        '''
1480
        Returns all :class:`~music21.note.NotRest` objects
1481

1482
        (will sometime become simply Note and Chord objects.)
1483

1484
        >>> s = stream.Stream()
1485
        >>> s.append(note.Note('C'))
1486
        >>> s.append(note.Rest())
1487
        >>> s.append(note.Note('D'))
1488
        >>> for el in s.iter().notes:
1489
        ...     print(el)
1490
        <music21.note.Note C>
1491
        <music21.note.Note D>
1492
        '''
1493
        return self.getElementsByClass(note.NotRest)
1✔
1494

1495
    @property
1✔
1496
    def notesAndRests(self):
1✔
1497
        '''
1498
        Returns all :class:`~music21.note.GeneralNote` objects, including
1499
        Rests and Unpitched elements.
1500

1501
        >>> s = stream.Stream()
1502
        >>> s.append(meter.TimeSignature('4/4'))
1503
        >>> s.append(note.Note('C'))
1504
        >>> s.append(note.Rest())
1505
        >>> s.append(note.Note('D'))
1506
        >>> for el in s.iter().notesAndRests:
1507
        ...     print(el)
1508
        <music21.note.Note C>
1509
        <music21.note.Rest quarter>
1510
        <music21.note.Note D>
1511

1512
        Chained filters (this makes no sense since notes is a subset of notesAndRests):
1513

1514
        >>> for el in s.iter().notesAndRests.notes:
1515
        ...     print(el)
1516
        <music21.note.Note C>
1517
        <music21.note.Note D>
1518
        '''
1519
        return self.getElementsByClass(note.GeneralNote)
1✔
1520

1521
    @property
1✔
1522
    def parts(self):
1✔
1523
        '''
1524
        Adds a ClassFilter for Part objects
1525
        '''
1526
        from music21 import stream
1✔
1527
        return self.getElementsByClass(stream.Part)
1✔
1528

1529
    @property
1✔
1530
    def spanners(self):
1✔
1531
        '''
1532
        Adds a ClassFilter for Spanner objects
1533
        '''
UNCOV
1534
        from music21 import spanner
×
UNCOV
1535
        return self.getElementsByClass(spanner.Spanner)
×
1536

1537
    @property
1✔
1538
    def voices(self):
1✔
1539
        '''
1540
        Adds a ClassFilter for Voice objects
1541
        '''
1542
        from music21 import stream
1✔
1543
        return self.getElementsByClass(stream.Voice)
1✔
1544

1545

1546
# -----------------------------------------------------------------------------
1547
class OffsetIterator(StreamIterator, Sequence[list[M21ObjType]]):
1✔
1548
    '''
1549
    An iterator that with each iteration returns a list of elements
1550
    that are at the same offset (or all at end)
1551

1552
    >>> s = stream.Stream()
1553
    >>> s.insert(0, note.Note('C'))
1554
    >>> s.insert(0, note.Note('D'))
1555
    >>> s.insert(1, note.Note('E'))
1556
    >>> s.insert(2, note.Note('F'))
1557
    >>> s.insert(2, note.Note('G'))
1558
    >>> s.storeAtEnd(bar.Repeat('end'))
1559
    >>> s.storeAtEnd(clef.TrebleClef())
1560

1561
    >>> oIter = stream.iterator.OffsetIterator(s)
1562
    >>> for groupedElements in oIter:
1563
    ...     print(groupedElements)
1564
    [<music21.note.Note C>, <music21.note.Note D>]
1565
    [<music21.note.Note E>]
1566
    [<music21.note.Note F>, <music21.note.Note G>]
1567
    [<music21.bar.Repeat direction=end>, <music21.clef.TrebleClef>]
1568

1569
    Does it work again?
1570

1571
    >>> for groupedElements2 in oIter:
1572
    ...     print(groupedElements2)
1573
    [<music21.note.Note C>, <music21.note.Note D>]
1574
    [<music21.note.Note E>]
1575
    [<music21.note.Note F>, <music21.note.Note G>]
1576
    [<music21.bar.Repeat direction=end>, <music21.clef.TrebleClef>]
1577

1578

1579
    >>> for groupedElements in oIter.notes:
1580
    ...     print(groupedElements)
1581
    [<music21.note.Note C>, <music21.note.Note D>]
1582
    [<music21.note.Note E>]
1583
    [<music21.note.Note F>, <music21.note.Note G>]
1584

1585
    >>> for groupedElements in stream.iterator.OffsetIterator(s).getElementsByClass(clef.Clef):
1586
    ...     print(groupedElements)
1587
    [<music21.clef.TrebleClef>]
1588
    '''
1589
    def __init__(self,
1✔
1590
                 srcStream,
1591
                 *,
1592
                 # restrictClass: type[M21ObjType] = base.Music21Object,
1593
                 filterList=None,
1594
                 restoreActiveSites=True,
1595
                 activeInformation=None,
1596
                 ignoreSorting=False
1597
                 ) -> None:
1598
        super().__init__(srcStream,
1✔
1599
                         # restrictClass=restrictClass,
1600
                         filterList=filterList,
1601
                         restoreActiveSites=restoreActiveSites,
1602
                         activeInformation=activeInformation,
1603
                         ignoreSorting=ignoreSorting,
1604
                         )
1605
        self.raiseStopIterationNext = False
1✔
1606
        self.nextToYield: list[M21ObjType] = []
1✔
1607
        self.nextOffsetToYield = None
1✔
1608

1609
    def __next__(self) -> list[M21ObjType]:  # type: ignore
1✔
1610
        if self.raiseStopIterationNext:
1✔
1611
            raise StopIteration
1✔
1612

1613
        retElementList: list[M21ObjType] = []
1✔
1614
        # make sure that cleanup is not called during the loop
1615
        try:
1✔
1616
            if self.nextToYield:
1✔
1617
                retElementList = self.nextToYield
1✔
1618
                retElOffset = self.nextOffsetToYield
1✔
1619
            else:
1620
                retEl = super().__next__()
1✔
1621
                retElOffset = self.srcStream.elementOffset(retEl)
1✔
1622
                retElementList = [retEl]
1✔
1623

1624
            # this inspection does not catch that we do return consistently
1625
            # because we catch the end of the while with the StopIteration
1626
            # noinspection PyInconsistentReturns
1627
            while self.elementIndex <= self.streamLength:
1✔
1628
                nextEl = super().__next__()
1✔
1629
                nextElOffset = self.srcStream.elementOffset(nextEl)
1✔
1630
                if nextElOffset == retElOffset:
1✔
1631
                    retElementList.append(nextEl)
1✔
1632
                else:
1633
                    self.nextToYield = [nextEl]
1✔
1634
                    self.nextOffsetToYield = nextElOffset
1✔
1635
                    self.activeInformation['lastYielded'] = retElementList[0]
1✔
1636
                    return retElementList
1✔
1637

1638
        except StopIteration:  # from the while statement.
1✔
1639
            if retElementList:
1✔
1640
                self.raiseStopIterationNext = True
1✔
1641
                self.activeInformation['lastYielded'] = retElementList[0]
1✔
1642
                return retElementList
1✔
1643
            else:
1644
                raise StopIteration
1✔
1645

1646
    def reset(self):
1✔
1647
        '''
1648
        runs before iteration
1649
        '''
1650
        super().reset()
1✔
1651
        self.nextToYield = []
1✔
1652
        self.nextOffsetToYield = None
1✔
1653
        self.raiseStopIterationNext = False
1✔
1654

1655
    # NOTE: these getElementsByClass are the same as the one in StreamIterator, but
1656
    # for now it needs to be duplicated until changing a Generic's argument type
1657
    # can be done with inheritance (Higher-Kinded Types).
1658
    @overload
1✔
1659
    def getElementsByClass(self,
1✔
1660
                           classFilterList: str,
1661
                           *,
1662
                           returnClone: bool = True) -> OffsetIterator[M21ObjType]:
UNCOV
1663
        ...
×
1664

1665
    @overload
1✔
1666
    def getElementsByClass(self,
1✔
1667
                           classFilterList: Iterable[str],
1668
                           *,
1669
                           returnClone: bool = True) -> OffsetIterator[M21ObjType]:
UNCOV
1670
        ...
×
1671

1672
    @overload
1✔
1673
    def getElementsByClass(self,
1✔
1674
                           classFilterList: type[ChangedM21ObjType],
1675
                           *,
1676
                           returnClone: bool = True) -> OffsetIterator[ChangedM21ObjType]:
UNCOV
1677
        ...
×
1678

1679
    # @overload
1680
    # def getElementsByClass(self,
1681
    #                        classFilterList: type,
1682
    #                        *,
1683
    #                        returnClone: bool = True) -> OffsetIterator[M21ObjType]:
1684
    #    ...
1685

1686
    @overload
1✔
1687
    def getElementsByClass(self,
1✔
1688
                           classFilterList: Iterable[type],
1689
                           *,
1690
                           returnClone: bool = True) -> OffsetIterator[M21ObjType]:
UNCOV
1691
        ...
×
1692

1693
    def getElementsByClass(self,
1✔
1694
                           classFilterList: t.Union[
1695
                               str,
1696
                               type[ChangedM21ObjType],
1697
                               Iterable[str],
1698
                               Iterable[type],
1699
                           ],
1700
                           *,
1701
                           returnClone: bool = True
1702
                           ) -> t.Union[OffsetIterator[M21ObjType],
1703
                                        OffsetIterator[ChangedM21ObjType]]:
1704
        '''
1705
        Identical to the same method in StreamIterator, but needs to be duplicated
1706
        for now.
1707
        '''
1708
        return self.addFilter(filters.ClassFilter(classFilterList), returnClone=returnClone)
1✔
1709

1710

1711

1712
# -----------------------------------------------------------------------------
1713
class RecursiveIterator(StreamIterator[M21ObjType], Sequence[M21ObjType]):
1✔
1714
    '''
1715
    One of the most powerful iterators in music21.  Generally not called
1716
    directly, but created by being invoked on a stream with `Stream.recurse()`
1717

1718
    >>> b = corpus.parse('bwv66.6')
1719
    >>> ri = stream.iterator.RecursiveIterator(b, streamsOnly=True)
1720
    >>> for x in ri:
1721
    ...     print(x)
1722
    <music21.stream.Part Soprano>
1723
    <music21.stream.Measure 0 offset=0.0>
1724
    <music21.stream.Measure 1 offset=1.0>
1725
    <music21.stream.Measure 2 offset=5.0>
1726
    ...
1727
    <music21.stream.Part Alto>
1728
    <music21.stream.Measure 0 offset=0.0>
1729
    ...
1730
    <music21.stream.Part Tenor>
1731
    ...
1732
    <music21.stream.Part Bass>
1733
    ...
1734

1735
    But this is how you'll actually use it:
1736

1737
    >>> for x in b.recurse(streamsOnly=True, includeSelf=True):
1738
    ...     print(x)
1739
    <music21.stream.Score bach/bwv66.6.mxl>
1740
    <music21.stream.Part Soprano>
1741
    <music21.stream.Measure 0 offset=0.0>
1742
    <music21.stream.Measure 1 offset=1.0>
1743
    <music21.stream.Measure 2 offset=5.0>
1744
    ...
1745
    <music21.stream.Part Alto>
1746
    <music21.stream.Measure 0 offset=0.0>
1747
    ...
1748
    <music21.stream.Part Tenor>
1749
    ...
1750
    <music21.stream.Part Bass>
1751
    ...
1752

1753
    >>> hasExpressions = lambda el, i: True if (hasattr(el, 'expressions')
1754
    ...       and el.expressions) else False
1755
    >>> expressive = b.recurse().addFilter(hasExpressions)
1756
    >>> expressive
1757
    <music21.stream.iterator.RecursiveIterator for Score:bach/bwv66.6.mxl @:0>
1758

1759
    >>> for el in expressive:
1760
    ...     print(el, el.expressions)
1761
    <music21.note.Note C#> [<music21.expressions.Fermata>]
1762
    <music21.note.Note A> [<music21.expressions.Fermata>]
1763
    <music21.note.Note F#> [<music21.expressions.Fermata>]
1764
    <music21.note.Note C#> [<music21.expressions.Fermata>]
1765
    <music21.note.Note G#> [<music21.expressions.Fermata>]
1766
    <music21.note.Note F#> [<music21.expressions.Fermata>]
1767

1768
    >>> len(expressive)
1769
    6
1770
    >>> expressive[-1].measureNumber
1771
    9
1772
    >>> bool(expressive)
1773
    True
1774
    '''
1775
    def __init__(
1✔
1776
        self,
1777
        srcStream,
1778
        *,
1779
        # restrictClass: type[M21ObjType] = base.Music21Object,
1780
        filterList=None,
1781
        restoreActiveSites=True,
1782
        activeInformation=None,
1783
        streamsOnly=False,
1784
        includeSelf=False,
1785
        ignoreSorting=False
1786
    ) -> None:  # , parentIterator=None):
1787
        super().__init__(srcStream,
1✔
1788
                         # restrictClass=restrictClass,
1789
                         filterList=filterList,
1790
                         restoreActiveSites=restoreActiveSites,
1791
                         activeInformation=activeInformation,
1792
                         ignoreSorting=ignoreSorting,
1793
                         )
1794
        self.returnSelf = includeSelf  # do I still need to return the self object?
1✔
1795
        self.includeSelf = includeSelf
1✔
1796
        self.ignoreSorting = ignoreSorting
1✔
1797

1798
        # within the list of parent/child recursive iterators, where does this start?
1799
        self.iteratorStartOffsetInHierarchy = 0.0
1✔
1800

1801
        if streamsOnly is True:
1✔
1802
            self.filters.append(filters.ClassFilter('Stream'))
1✔
1803
        self.childRecursiveIterator: RecursiveIterator[t.Any]|None = None
1✔
1804
        # not yet used.
1805
        # self.parentIterator = None
1806

1807
    def __next__(self) -> M21ObjType:
1✔
1808
        '''
1809
        Get the next element of the stream under iteration.
1810

1811
        The same __iter__ as the superclass is used.
1812
        '''
1813
        while self.elementIndex < self.streamLength:
1✔
1814
            # wrap this in a while loop instead of
1815
            # returning self.__next__() because
1816
            # in a long score with a miserly filter
1817
            # it is possible to exceed maximum recursion
1818
            # depth
1819
            if self.childRecursiveIterator is not None:
1✔
1820
                try:
1✔
1821
                    return next(self.childRecursiveIterator)
1✔
1822
                except StopIteration:
1✔
1823
                    # self.childRecursiveIterator.parentIterator = None
1824
                    self.childRecursiveIterator = None
1✔
1825

1826
            if self.returnSelf is True and self.matchesFilters(self.srcStream):
1✔
1827
                self.activeInformation['stream'] = None
1✔
1828
                self.activeInformation['elementIndex'] = -1
1✔
1829
                self.activeInformation['lastYielded'] = self.srcStream
1✔
1830
                self.returnSelf = False
1✔
1831
                return t.cast(M21ObjType, self.srcStream)
1✔
1832

1833
            elif self.returnSelf is True:
1✔
UNCOV
1834
                self.returnSelf = False
×
1835

1836
            if self.elementIndex >= self.elementsLength:
1✔
1837
                self.iterSection = '_endElements'
1✔
1838
                self.sectionIndex = self.elementIndex - self.elementsLength
1✔
1839
            else:
1840
                self.sectionIndex = self.elementIndex
1✔
1841

1842
            try:
1✔
1843
                e = self.srcStreamElements[self.elementIndex]
1✔
UNCOV
1844
            except IndexError:
×
UNCOV
1845
                self.elementIndex += 1
×
1846
                # this may happen if the number of elements has changed
1847
                continue
×
1848

1849
            self.elementIndex += 1
1✔
1850

1851
            # in a recursive filter, the stream does not need to match the filter,
1852
            # only the internal elements.
1853
            if e.isStream:
1✔
1854
                if t.TYPE_CHECKING:
1855
                    assert isinstance(e, streamModule.Stream)
1856

1857
                childRecursiveIterator: RecursiveIterator[M21ObjType] = RecursiveIterator(
1✔
1858
                    srcStream=e,
1859
                    restoreActiveSites=self.restoreActiveSites,
1860
                    filterList=self.filters,  # shared list
1861
                    activeInformation=self.activeInformation,  # shared dict
1862
                    includeSelf=False,  # always for inner streams
1863
                    ignoreSorting=self.ignoreSorting,
1864
                    # parentIterator=self,
1865
                )
1866
                newStartOffset = (self.iteratorStartOffsetInHierarchy
1✔
1867
                                  + self.srcStream.elementOffset(e))
1868

1869
                childRecursiveIterator.iteratorStartOffsetInHierarchy = newStartOffset
1✔
1870
                self.childRecursiveIterator = childRecursiveIterator
1✔
1871
            if self.matchesFilters(e) is False:
1✔
1872
                continue
1✔
1873

1874
            if self.restoreActiveSites is True:
1✔
1875
                self.srcStream.coreSelfActiveSite(e)
1✔
1876

1877
            self.updateActiveInformation()
1✔
1878
            self.activeInformation['lastYielded'] = e
1✔
1879
            return e
1✔
1880

1881
        # the last element can still set a recursive iterator, so make sure we handle it.
1882
        if self.childRecursiveIterator is not None:
1✔
1883
            try:
1✔
1884
                return next(self.childRecursiveIterator)
1✔
1885
            except StopIteration:
1✔
1886
                # self.childRecursiveIterator.parentIterator = None
1887
                self.childRecursiveIterator = None
1✔
1888

1889
        self.activeInformation['lastYielded'] = None  # always clean this up, no matter what
1✔
1890
        self.cleanup()
1✔
1891
        raise StopIteration
1✔
1892

1893
    def reset(self):
1✔
1894
        '''
1895
        reset prior to iteration
1896
        '''
1897
        self.returnSelf = self.includeSelf
1✔
1898
        self.childRecursiveIterator = None
1✔
1899
        super().reset()
1✔
1900

1901
    def matchingElements(self, *, restoreActiveSites=True):
1✔
1902
        # saved parent iterator later?
1903
        # will this work in mid-iteration? Test, or do not expose till then.
1904
        with tempAttribute(self, 'childRecursiveIterator'):
1✔
1905
            fe = super().matchingElements(restoreActiveSites=restoreActiveSites)
1✔
1906
        return fe
1✔
1907

1908
    def iteratorStack(self) -> list[RecursiveIterator]:
1✔
1909
        '''
1910
        Returns a stack of RecursiveIterators at this point in the iteration.  Last is most recent.
1911

1912
        >>> b = corpus.parse('bwv66.6')
1913
        >>> bRecurse = b.recurse()
1914
        >>> i = 0
1915
        >>> for _ in bRecurse:
1916
        ...     i += 1
1917
        ...     if i > 13:
1918
        ...         break
1919
        >>> bRecurse.iteratorStack()
1920
        [<music21.stream.iterator.RecursiveIterator for Score:bach/bwv66.6.mxl @:2>,
1921
         <music21.stream.iterator.RecursiveIterator for Part:Soprano @:3>,
1922
         <music21.stream.iterator.RecursiveIterator for Measure:m.1 @:3>]
1923
        '''
1924
        iterStack = [self]
1✔
1925
        x = self
1✔
1926
        while x.childRecursiveIterator is not None:
1✔
1927
            x = x.childRecursiveIterator
1✔
1928
            iterStack.append(x)
1✔
1929
        return iterStack
1✔
1930

1931
    def streamStack(self):
1✔
1932
        '''
1933
        Returns a stack of Streams at this point.  Last is most recent.
1934

1935
        However, the current element may be the same as the last element in the stack
1936

1937
        >>> b = corpus.parse('bwv66.6')
1938
        >>> bRecurse = b.recurse()
1939
        >>> i = 0
1940
        >>> for x in bRecurse:
1941
        ...     i += 1
1942
        ...     if i > 12:
1943
        ...         break
1944
        >>> bRecurse.streamStack()
1945
        [<music21.stream.Score bach/bwv66.6.mxl>,
1946
         <music21.stream.Part Soprano>,
1947
         <music21.stream.Measure 1 offset=1.0>]
1948
        '''
1949
        return [i.srcStream for i in self.iteratorStack()]
1✔
1950

1951
    def currentHierarchyOffset(self):
1✔
1952
        '''
1953
        Called on the current iterator, returns the current offset in the hierarchy.
1954
        Or None if we are not currently iterating.
1955

1956
        >>> b = corpus.parse('bwv66.6')
1957
        >>> bRecurse = b.recurse().notes
1958
        >>> print(bRecurse.currentHierarchyOffset())
1959
        None
1960
        >>> for n in bRecurse:
1961
        ...     print(n.measureNumber, bRecurse.currentHierarchyOffset(), n)
1962
        0 0.0 <music21.note.Note C#>
1963
        0 0.5 <music21.note.Note B>
1964
        1 1.0 <music21.note.Note A>
1965
        1 2.0 <music21.note.Note B>
1966
        1 3.0 <music21.note.Note C#>
1967
        1 4.0 <music21.note.Note E>
1968
        2 5.0 <music21.note.Note C#>
1969
        ...
1970
        9 34.5 <music21.note.Note E#>
1971
        9 35.0 <music21.note.Note F#>
1972
        0 0.0 <music21.note.Note E>
1973
        1 1.0 <music21.note.Note F#>
1974
        ...
1975

1976
        After iteration completes, the figure is reset to None:
1977

1978
        >>> print(bRecurse.currentHierarchyOffset())
1979
        None
1980

1981
        The offsets are with respect to the position inside the stream
1982
        being iterated, so, for instance, this will not change the output from above:
1983

1984
        >>> o = stream.Opus()
1985
        >>> o.insert(20.0, b)
1986
        >>> bRecurse = b.recurse().notes
1987
        >>> for n in bRecurse:
1988
        ...     print(n.measureNumber, bRecurse.currentHierarchyOffset(), n)
1989
        0 0.0 <music21.note.Note C#>
1990
        ...
1991

1992
        But of course, this will add 20.0 to all numbers:
1993

1994
        >>> oRecurse = o.recurse().notes
1995
        >>> for n in oRecurse:
1996
        ...     print(n.measureNumber, oRecurse.currentHierarchyOffset(), n)
1997
        0 20.0 <music21.note.Note C#>
1998
        ...
1999

2000
        * New in v4.
2001
        '''
2002
        lastYield = self.activeInformation['lastYielded']
1✔
2003
        if lastYield is None:
1✔
2004
            return None
1✔
2005

2006
        iteratorStack = self.iteratorStack()
1✔
2007
        newestIterator = iteratorStack[-1]
1✔
2008
        lastStream = newestIterator.srcStream
1✔
2009
        lastStartOffset = newestIterator.iteratorStartOffsetInHierarchy
1✔
2010

2011
        if lastYield is lastStream:
1✔
2012
            return common.opFrac(lastStartOffset)
1✔
2013
        else:
2014
            return common.opFrac(lastStartOffset + lastStream.elementOffset(lastYield))
1✔
2015
            # will still return numbers even if _endElements
2016

2017
    def getElementsByOffsetInHierarchy(
1✔
2018
            self: StreamIteratorType,
2019
            offsetStart,
2020
            offsetEnd=None,
2021
            *,
2022
            includeEndBoundary=True,
2023
            mustFinishInSpan=False,
2024
            mustBeginInSpan=True,
2025
            includeElementsThatEndAtStart=True) -> StreamIteratorType:
2026
        '''
2027
        Adds a filter keeping only Music21Objects that
2028
        are found at a certain offset or within a certain
2029
        offset time range (given the `offsetStart` and optional `offsetEnd` values) from
2030
        the beginning of the hierarchy.
2031

2032
        >>> b = corpus.parse('bwv66.6')
2033
        >>> for n in b.recurse().getElementsByOffsetInHierarchy(8, 9.5).notes:
2034
        ...     print(n,
2035
        ...           n.getOffsetInHierarchy(b),
2036
        ...           n.measureNumber,
2037
        ...           n.getContextByClass(stream.Part).id)
2038
        <music21.note.Note C#> 8.0 2 Soprano
2039
        <music21.note.Note A> 9.0 3 Soprano
2040
        <music21.note.Note B> 9.5 3 Soprano
2041
        <music21.note.Note G#> 8.0 2 Alto
2042
        <music21.note.Note F#> 9.0 3 Alto
2043
        <music21.note.Note G#> 9.5 3 Alto
2044
        <music21.note.Note C#> 8.0 2 Tenor
2045
        <music21.note.Note C#> 9.0 3 Tenor
2046
        <music21.note.Note D> 9.5 3 Tenor
2047
        <music21.note.Note E#> 8.0 2 Bass
2048
        <music21.note.Note F#> 9.0 3 Bass
2049
        <music21.note.Note B> 9.5 3 Bass
2050

2051
        * Changed in v5.5: all behavior-changing options are keyword only.
2052
        '''
2053
        f = filters.OffsetHierarchyFilter(
1✔
2054
            offsetStart,
2055
            offsetEnd,
2056
            includeEndBoundary=includeEndBoundary,
2057
            mustFinishInSpan=mustFinishInSpan,
2058
            mustBeginInSpan=mustBeginInSpan,
2059
            includeElementsThatEndAtStart=includeElementsThatEndAtStart)
2060
        return self.addFilter(f)
1✔
2061

2062
    @overload
1✔
2063
    def getElementsByClass(self,
1✔
2064
                           classFilterList: str,
2065
                           *,
2066
                           returnClone: bool = True) -> RecursiveIterator[M21ObjType]:
UNCOV
2067
        ...
×
2068

2069
    @overload
1✔
2070
    def getElementsByClass(self,
1✔
2071
                           classFilterList: Iterable[str],
2072
                           *,
2073
                           returnClone: bool = True) -> RecursiveIterator[M21ObjType]:
UNCOV
2074
        ...
×
2075

2076
    @overload
1✔
2077
    def getElementsByClass(self,
1✔
2078
                           classFilterList: type[ChangedM21ObjType],
2079
                           *,
2080
                           returnClone: bool = True) -> RecursiveIterator[ChangedM21ObjType]:
UNCOV
2081
        ...
×
2082

2083
    # @overload
2084
    # def getElementsByClass(self,
2085
    #                        classFilterList: type,
2086
    #                        *,
2087
    #                        returnClone: bool = True) -> RecursiveIterator[M21ObjType]:
2088
    #     ...
2089

2090
    @overload
1✔
2091
    def getElementsByClass(self,
1✔
2092
                           classFilterList: Iterable[type],
2093
                           *,
2094
                           returnClone: bool = True) -> RecursiveIterator[M21ObjType]:
UNCOV
2095
        ...
×
2096

2097

2098
    def getElementsByClass(self,
1✔
2099
                           classFilterList: t.Union[
2100
                               str,
2101
                               type[ChangedM21ObjType],
2102
                               Iterable[str],
2103
                               Iterable[type[ChangedM21ObjType]],
2104
                           ],
2105
                           *,
2106
                           returnClone: bool = True
2107
                           ) -> t.Union[RecursiveIterator[M21ObjType],
2108
                                        RecursiveIterator[ChangedM21ObjType]]:
2109
        out = super().getElementsByClass(classFilterList, returnClone=returnClone)
1✔
2110
        if isinstance(classFilterList, type) and issubclass(classFilterList, base.Music21Object):
1✔
2111
            return t.cast(RecursiveIterator[ChangedM21ObjType], out)
1✔
2112
        else:
2113
            return t.cast(RecursiveIterator[M21ObjType], out)
1✔
2114

2115

2116
class Test(unittest.TestCase):
1✔
2117
    def testSimpleClone(self):
1✔
2118
        from music21 import stream
1✔
2119
        s = stream.Stream()
1✔
2120
        r = note.Rest()
1✔
2121
        n = note.Note()
1✔
2122
        s.append([r, n])
1✔
2123
        all_s = list(s.iter())
1✔
2124
        self.assertEqual(len(all_s), 2)
1✔
2125
        self.assertIs(all_s[0], r)
1✔
2126
        self.assertIs(all_s[1], n)
1✔
2127
        s_notes = list(s.iter().notes)
1✔
2128
        self.assertEqual(len(s_notes), 1)
1✔
2129
        self.assertIs(s_notes[0], n)
1✔
2130

2131
    def testAddingFiltersMidIteration(self):
1✔
2132
        from music21 import stream
1✔
2133
        s = stream.Stream()
1✔
2134
        r = note.Rest()
1✔
2135
        n = note.Note()
1✔
2136
        s.append([r, n])
1✔
2137
        sIter = s.iter()
1✔
2138
        r0 = next(sIter)
1✔
2139
        self.assertIs(r0, r)
1✔
2140

2141
        # adding a filter gives a new StreamIterator that restarts at 0
2142
        sIter2 = sIter.notesAndRests  # this filter does nothing here.
1✔
2143
        obj0 = next(sIter2)
1✔
2144
        self.assertIs(obj0, r)
1✔
2145

2146
        # the original StreamIterator should be at its original spot, so this should
2147
        # move to the next element
2148
        n0 = next(sIter)
1✔
2149
        self.assertIs(n0, n)
1✔
2150

2151
    def testRecursiveActiveSites(self):
1✔
2152
        from music21 import converter
1✔
2153
        s = converter.parse('tinyNotation: 4/4 c1 c4 d=id2 e f')
1✔
2154
        rec = s.recurse()
1✔
2155
        n = rec.getElementById('id2')
1✔
2156
        self.assertEqual(n.activeSite.number, 2)
1✔
2157

2158
    def testCurrentHierarchyOffsetReset(self):
1✔
2159
        from music21 import stream
1✔
2160
        p = stream.Part()
1✔
2161
        m = stream.Measure()
1✔
2162
        m.append(note.Note('D'))
1✔
2163
        m.append(note.Note('E'))
1✔
2164
        p.insert(0, note.Note('C'))
1✔
2165
        p.append(m)
1✔
2166
        pRecurse = p.recurse(includeSelf=True)
1✔
2167
        allOffsets = []
1✔
2168
        for _ in pRecurse:
1✔
2169
            allOffsets.append(pRecurse.currentHierarchyOffset())
1✔
2170
        self.assertListEqual(allOffsets, [0.0, 0.0, 1.0, 1.0, 2.0])
1✔
2171
        currentOffset = pRecurse.currentHierarchyOffset()
1✔
2172
        self.assertIsNone(currentOffset)
1✔
2173

2174
    def testAddingFiltersMidRecursiveIteration(self):
1✔
2175
        from music21 import stream
1✔
2176
        # noinspection PyUnresolvedReferences
2177
        from music21.stream.iterator import RecursiveIterator as ImportedRecursiveIterator
1✔
2178
        m = stream.Measure()
1✔
2179
        r = note.Rest()
1✔
2180
        n = note.Note()
1✔
2181
        m.append([r, n])
1✔
2182
        p = stream.Part()
1✔
2183
        p.append(m)
1✔
2184

2185
        sc = stream.Score()
1✔
2186
        sc.append(p)
1✔
2187

2188
        sIter = sc.recurse()
1✔
2189
        p0 = next(sIter)
1✔
2190
        self.assertIs(p0, p)
1✔
2191

2192
        child = sIter.childRecursiveIterator
1✔
2193
        self.assertIsInstance(child, ImportedRecursiveIterator)
1✔
2194

2195

2196

2197

2198
_DOC_ORDER = [StreamIterator, RecursiveIterator, OffsetIterator]
1✔
2199

2200
if __name__ == '__main__':
2201
    import music21
2202
    music21.mainTest(Test)  # , runTest='testCurrentHierarchyOffsetReset')
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc