1# -*- coding: utf-8 -*- 2# ----------------------------------------------------------------------------- 3# Name: stream/iterator.py 4# Purpose: classes for walking through streams... 5# 6# Authors: Michael Scott Cuthbert 7# Christopher Ariza 8# 9# Copyright: Copyright © 2008-2016 Michael Scott Cuthbert and the music21 Project 10# License: BSD, see license.txt 11# ----------------------------------------------------------------------------- 12''' 13this class contains iterators and filters for walking through streams 14 15StreamIterators are explicitly allowed to access private methods on streams. 16''' 17import copy 18from typing import TypeVar, List, Union, Callable, Optional, Dict 19import unittest 20import warnings 21 22from music21 import common 23from music21.common.classTools import tempAttribute, saveAttributes 24from music21.common.enums import OffsetSpecial 25from music21.exceptions21 import StreamException 26from music21.stream import filters 27from music21 import prebase 28from music21 import base # just for typing. 29 30from music21.sites import SitesException 31 32 33_SIter = TypeVar('_SIter') 34FilterType = Union[Callable, filters.StreamFilter] 35 36# ----------------------------------------------------------------------------- 37 38 39class StreamIteratorException(StreamException): 40 pass 41 42 43class StreamIteratorInefficientWarning(PendingDeprecationWarning): 44 pass 45 46# ----------------------------------------------------------------------------- 47 48 49class StreamIterator(prebase.ProtoM21Object): 50 ''' 51 An Iterator object used to handle getting items from Streams. 52 The :meth:`~music21.stream.Stream.__iter__` method 53 returns this object, passing a reference to self. 54 55 Note that this iterator automatically sets the active site of 56 returned elements to the source Stream. 57 58 There is one property to know about: .overrideDerivation which overrides the set 59 derivation of the class when .stream() is called 60 61 Sets: 62 63 * StreamIterator.srcStream -- the Stream iterated over 64 * StreamIterator.index -- current index item 65 * StreamIterator.streamLength -- length of elements. 66 67 * StreamIterator.srcStreamElements -- srcStream._elements 68 * StreamIterator.cleanupOnStop -- should the StreamIterator delete the 69 reference to srcStream and srcStreamElements when stopping? default 70 False 71 * StreamIterator.activeInformation -- a dict that contains information 72 about where we are in the parse. Especially useful for recursive 73 streams. 'stream' = the stream that is currently active, 'index' 74 where in `.elements` we are, `iterSection` is `_elements` or `_endElements`, 75 and `sectionIndex` is where we are in the iterSection, or -1 if 76 we have not started. This dict is shared among all sub iterators. 77 78 Constructor keyword only arguments: 79 80 * `filterList` is a list of stream.filters.Filter objects to apply 81 82 * if `restoreActiveSites` is True (default) then on iterating, the activeSite is set 83 to the Stream being iterated over. 84 85 * if `ignoreSorting` is True (default is False) then the Stream is not sorted before 86 iterating. If the Stream is already sorted, then this value does not matter, and 87 no time will be saved by setting to False. 88 89 * For `activeInformation` see above. 90 91 Changed in v.5.2 -- all arguments except srcStream are keyword only. 92 93 OMIT_FROM_DOCS 94 95 Informative exception for user error: 96 97 >>> s = stream.Stream() 98 >>> sIter = stream.iterator.StreamIterator(s, filterList=[note.Note]) 99 Traceback (most recent call last): 100 TypeError: filterList expects Filters or callables, 101 not types themselves; got <class 'music21.note.Note'> 102 ''' 103 def __init__(self, 104 srcStream: 'music21.stream.Stream', 105 *, 106 filterList: Union[List[FilterType], FilterType, None] = None, 107 restoreActiveSites: bool = True, 108 activeInformation: Optional[Dict] = None, 109 ignoreSorting: bool = False): 110 if not ignoreSorting and srcStream.isSorted is False and srcStream.autoSort: 111 srcStream.sort() 112 self.srcStream: 'music21.stream.Stream' = srcStream 113 self.index: int = 0 114 115 # use .elements instead of ._elements/etc. so that it is sorted... 116 self.srcStreamElements = srcStream.elements 117 self.streamLength = len(self.srcStreamElements) 118 119 # this information can help in speed later 120 self.elementsLength = len(self.srcStream._elements) 121 self.sectionIndex = -1 # where we are within a given section (_elements or _endElements) 122 self.iterSection = '_elements' 123 124 self.cleanupOnStop = False 125 self.restoreActiveSites: bool = restoreActiveSites 126 127 self.overrideDerivation = None 128 129 if filterList is None: 130 filterList = [] 131 elif not common.isIterable(filterList): 132 filterList = [filterList] 133 elif isinstance(filterList, (set, tuple)): 134 filterList = list(filterList) # mutable.... 135 for x in filterList: 136 if isinstance(x, type): 137 raise TypeError( 138 f'filterList expects Filters or callables, not types themselves; got {x}') 139 # self.filters is a list of expressions that 140 # return True or False for an element for 141 # whether it should be yielded. 142 self.filters: List[FilterType] = filterList 143 self._len = None 144 self._matchingElements = None 145 146 # keep track of where we are in the parse. 147 # esp important for recursive streams... 148 if activeInformation is not None: 149 self.activeInformation = activeInformation 150 else: 151 self.activeInformation = {} # in Py3.8 make a TypedDict 152 self.updateActiveInformation() 153 154 def _reprInternal(self): 155 streamClass = self.srcStream.__class__.__name__ 156 srcStreamId = self.srcStream.id 157 try: 158 srcStreamId = hex(srcStreamId) 159 except TypeError: 160 pass 161 162 if streamClass == 'Measure' and self.srcStream.number != 0: 163 srcStreamId = 'm.' + str(self.srcStream.number) 164 165 return f'for {streamClass}:{srcStreamId} @:{self.index}' 166 167 def __call__(self) -> _SIter: 168 ''' 169 Temporary workaround to support both prior usage of `.iter` 170 and new recommended usage of `.iter()`. 171 During the period where `.iter` is still supported, even calling `.iter()` 172 (recommended) will retrieve the property `.iter` and necessitate 173 this workaround. 174 175 Returns `self` without any changes. 176 177 TODO: manage and emit DeprecationWarnings in v.8 178 TODO: remove in v.9 179 ''' 180 return self 181 182 def __iter__(self): 183 self.reset() 184 return self 185 186 def __next__(self) -> base.Music21Object: 187 while self.index < self.streamLength: 188 if self.index >= self.elementsLength: 189 self.iterSection = '_endElements' 190 self.sectionIndex = self.index - self.elementsLength 191 else: 192 self.sectionIndex = self.index 193 194 try: 195 e = self.srcStreamElements[self.index] 196 except IndexError: 197 # this may happen if the number of elements has changed 198 self.index += 1 199 continue 200 201 self.index += 1 202 if self.matchesFilters(e) is False: 203 continue 204 205 if self.restoreActiveSites is True: 206 self.srcStream.coreSelfActiveSite(e) 207 208 self.updateActiveInformation() 209 return e 210 211 self.cleanup() 212 raise StopIteration 213 214 def __getattr__(self, attr): 215 ''' 216 In case an attribute is defined on Stream but not on a StreamIterator, 217 create a Stream and then return that attribute. This is NOT performance 218 optimized -- calling this repeatedly will mean creating a lot of different 219 streams. However, it will prevent most code that worked on v.2. from breaking 220 on v.3 and onwards. 221 222 >>> s = stream.Measure() 223 >>> s.insert(0, note.Rest()) 224 >>> s.repeatAppend(note.Note('C'), 2) 225 226 >>> s.definesExplicitSystemBreaks 227 False 228 229 >>> s.notes 230 <music21.stream.iterator.StreamIterator for Measure:0x101c1a208 @:0> 231 232 >>> import warnings #_DOCS_HIDE 233 >>> SIIW = stream.iterator.StreamIteratorInefficientWarning #_DOCS_HIDE 234 >>> with warnings.catch_warnings(): #_DOCS_HIDE 235 ... warnings.simplefilter('ignore', SIIW) #_DOCS_HIDE 236 ... explicit = s.notes.definesExplicitSystemBreaks #_DOCS_HIDE 237 >>> #_DOCS_SHOW explicit = s.notes.definesExplicitSystemBreaks 238 >>> explicit 239 False 240 241 Works with methods as well: 242 243 >>> with warnings.catch_warnings(): #_DOCS_HIDE 244 ... warnings.simplefilter('ignore', SIIW) #_DOCS_HIDE 245 ... popC = s.notes.pop(0) #_DOCS_HIDE 246 >>> #_DOCS_SHOW popC = s.notes.pop(0) 247 >>> popC 248 <music21.note.Note C> 249 250 But remember that a new Stream is being created each time that an attribute 251 only defined on a Stream is called, so for instance, so you can pop() forever, 252 always getting the same element. 253 254 >>> with warnings.catch_warnings(): #_DOCS_HIDE 255 ... warnings.simplefilter('ignore', SIIW) #_DOCS_HIDE 256 ... popC = s.notes.pop(0) #_DOCS_HIDE 257 >>> #_DOCS_SHOW popC = s.notes.pop(0) 258 >>> popC 259 <music21.note.Note C> 260 >>> with warnings.catch_warnings(): #_DOCS_HIDE 261 ... warnings.simplefilter('ignore', SIIW) #_DOCS_HIDE 262 ... popC = s.notes.pop(0) #_DOCS_HIDE 263 >>> #_DOCS_SHOW popC = s.notes.pop(0) 264 >>> popC 265 <music21.note.Note C> 266 >>> with warnings.catch_warnings(): #_DOCS_HIDE 267 ... warnings.simplefilter('ignore', SIIW) #_DOCS_HIDE 268 ... popC = s.notes.pop(0) #_DOCS_HIDE 269 >>> #_DOCS_SHOW popC = s.notes.pop(0) 270 >>> popC 271 <music21.note.Note C> 272 273 If run with -w, this call will send a StreamIteratorInefficientWarning to stderr 274 reminding developers that this is not an efficient call, and .stream() should be 275 called (and probably cached) explicitly. 276 277 Failures are explicitly given as coming from the StreamIterator object. 278 279 >>> s.asdf 280 Traceback (most recent call last): 281 AttributeError: 'Measure' object has no attribute 'asdf' 282 >>> s.notes.asdf 283 Traceback (most recent call last): 284 AttributeError: 'StreamIterator' object has no attribute 'asdf' 285 286 OMIT_FROM_DOCS 287 288 srcStream is accessible, but not with "__getattr__", which joblib uses 289 290 >>> s.notes.srcStream is s 291 True 292 >>> s.notes.__getattr__('srcStream') is None 293 True 294 ''' 295 # Prevent infinite loop in feature extractor task serialization 296 # TODO: investigate if this can be removed once iter becomes iter() 297 if attr == 'srcStream': 298 return None 299 300 if not hasattr(self.srcStream, attr): 301 # original stream did not have the attribute, so new won't; but raise on iterator. 302 raise AttributeError(f'{self.__class__.__name__!r} object has no attribute {attr!r}') 303 304 warnings.warn( 305 attr + ' is not defined on StreamIterators. Call .stream() first for efficiency', 306 StreamIteratorInefficientWarning, 307 stacklevel=2) 308 309 sOut = self.stream() 310 return getattr(sOut, attr) 311 312 def __getitem__(self, k): 313 ''' 314 if you are in the iterator, you should still be able to request other items... 315 uses self.srcStream.__getitem__ 316 317 >>> s = stream.Stream() 318 >>> s.insert(0, note.Note('F#')) 319 >>> s.repeatAppend(note.Note('C'), 2) 320 >>> sI = s.iter() 321 >>> sI 322 <music21.stream.iterator.StreamIterator for Stream:0x104743be0 @:0> 323 324 >>> sI.srcStream is s 325 True 326 327 >>> for n in sI: 328 ... printer = (repr(n), repr(sI[0])) 329 ... print(printer) 330 ('<music21.note.Note F#>', '<music21.note.Note F#>') 331 ('<music21.note.Note C>', '<music21.note.Note F#>') 332 ('<music21.note.Note C>', '<music21.note.Note F#>') 333 >>> sI.srcStream is s 334 True 335 336 Slices work: 337 338 >>> nSlice = sI[1:] 339 >>> for n in nSlice: 340 ... print(n) 341 <music21.note.Note C> 342 <music21.note.Note C> 343 344 Filters, such as "notes" apply. 345 346 >>> s.insert(0, clef.TrebleClef()) 347 >>> s[0] 348 <music21.clef.TrebleClef> 349 >>> s.iter().notes[0] 350 <music21.note.Note F#> 351 352 Demo of cleanupOnStop = True 353 354 >>> sI.cleanupOnStop = True 355 >>> for n in sI: 356 ... printer = (repr(n), repr(sI[0])) 357 ... print(printer) 358 ('<music21.note.Note F#>', '<music21.note.Note F#>') 359 ('<music21.note.Note C>', '<music21.note.Note F#>') 360 ('<music21.note.Note C>', '<music21.note.Note F#>') 361 >>> sI.srcStream is None 362 True 363 >>> for n in sI: 364 ... printer = (repr(n), repr(sI[0])) 365 ... print(printer) 366 367 (nothing is printed) 368 ''' 369 fe = self.matchingElements() 370 try: 371 e = fe[k] 372 except TypeError: 373 e = None 374 for el in fe: 375 if el.id.lower() == k.lower(): 376 e = el 377 break 378 # TODO: Slices and everything else in Stream __getitem__ ; in fact, merge... 379 return e 380 381 def __len__(self) -> int: 382 ''' 383 returns the length of the elements that 384 match the filter set. 385 386 >>> s = converter.parse('tinynotation: 3/4 c4 d e f g a', makeNotation=False) 387 >>> len(s) 388 7 389 >>> len(s.iter()) 390 7 391 >>> len(s.iter().notes) 392 6 393 >>> [n.name for n in s.iter().notes] 394 ['C', 'D', 'E', 'F', 'G', 'A'] 395 ''' 396 if self._len is not None: 397 return self._len 398 self._len = len(self.matchingElements(restoreActiveSites=False)) 399 self.reset() 400 return self._len 401 402 def __bool__(self) -> bool: 403 ''' 404 return True if anything matches the filter 405 otherwise, return False 406 407 >>> s = converter.parse('tinyNotation: 2/4 c4 r4') 408 >>> bool(s) 409 True 410 >>> iterator = s.recurse() 411 >>> bool(iterator) 412 True 413 >>> bool(iterator.notesAndRests) 414 True 415 >>> bool(iterator.notes) 416 True 417 418 test cache 419 420 >>> len(iterator.notes) 421 1 422 >>> bool(iterator.notes) 423 True 424 >>> bool(iterator.notes) 425 True 426 427 >>> iterator = s.recurse() 428 >>> bool(iterator) 429 True 430 >>> bool(iterator) 431 True 432 >>> bool(iterator) 433 True 434 435 >>> bool(iterator.getElementsByClass('Chord')) 436 False 437 438 test false cache: 439 440 >>> len(iterator.getElementsByClass('Chord')) 441 0 442 >>> bool(iterator.getElementsByClass('Chord')) 443 False 444 445 ''' 446 if self._len is not None: 447 return bool(self._len) 448 449 # do not change active site of first element in bool 450 with tempAttribute(self, 'restoreActiveSites', False): 451 for unused in self: 452 return True 453 454 return False 455 456 def clone(self: _SIter) -> _SIter: 457 ''' 458 Returns a new copy of the same iterator. 459 (a shallow copy of some things except activeInformation) 460 ''' 461 out: _SIter = type(self)( 462 self.srcStream, 463 filterList=copy.copy(self.filters), 464 restoreActiveSites=self.restoreActiveSites, 465 activeInformation=copy.copy(self.activeInformation), 466 ) 467 return out 468 469 def first(self) -> Optional[base.Music21Object]: 470 ''' 471 Efficiently return the first matching element, or None if no 472 elements match. 473 474 Does not require creating the whole list of matching elements. 475 476 >>> s = converter.parse('tinyNotation: 3/4 D4 E2 F4 r2 G2 r4') 477 >>> s.recurse().notes.first() 478 <music21.note.Note D> 479 >>> s.recurse().getElementsByClass('Rest').first() 480 <music21.note.Rest half> 481 482 If no elements match, returns None: 483 484 >>> print(s.recurse().getElementsByClass('Chord').first()) 485 None 486 487 New in v7. 488 489 OMIT_FROM_DOCS 490 491 Ensure that next continues after the first note running: 492 493 >>> notes = s.recurse().notes 494 >>> notes.first() 495 <music21.note.Note D> 496 >>> next(notes) 497 <music21.note.Note E> 498 499 Now reset on new iteration: 500 501 >>> for n in notes: 502 ... print(n) 503 <music21.note.Note D> 504 <music21.note.Note E> 505 ... 506 507 An Empty stream: 508 509 >>> s = stream.Stream() 510 >>> s.iter().notes.first() is None 511 True 512 ''' 513 iter(self) 514 try: 515 return next(self) 516 except StopIteration: 517 return None 518 519 def last(self) -> Optional[base.Music21Object]: 520 ''' 521 Returns the last matching element, or None if no elements match. 522 523 Currently is not efficient (does not iterate backwards, for instance), 524 but easier than checking for an IndexError. Might be refactored later 525 to iterate the stream backwards instead if it gets a lot of use. 526 527 >>> s = converter.parse('tinyNotation: 3/4 D4 E2 F4 r2 G2 r4') 528 >>> s.recurse().notes.last() 529 <music21.note.Note G> 530 >>> s.recurse().getElementsByClass('Rest').last() 531 <music21.note.Rest quarter> 532 533 New in v7. 534 535 OMIT_FROM_DOCS 536 537 Check on empty Stream: 538 539 >>> s2 = stream.Stream() 540 >>> s2.iter().notes.last() is None 541 True 542 543 Next has a different feature from first(), will start again from beginning. 544 This behavior may change. 545 546 >>> notes = s.recurse().notes 547 >>> notes.last() 548 <music21.note.Note G> 549 >>> next(notes) 550 <music21.note.Note D> 551 ''' 552 fe = self.matchingElements() 553 if not fe: 554 return None 555 return fe[-1] 556 557 # --------------------------------------------------------------- 558 # start and stop 559 def updateActiveInformation(self): 560 ''' 561 Updates the (shared) activeInformation dictionary 562 with information about 563 where we are. 564 565 Call before any element return 566 ''' 567 ai = self.activeInformation 568 ai['stream'] = self.srcStream 569 ai['index'] = self.index - 1 570 ai['iterSection'] = self.iterSection 571 ai['sectionIndex'] = self.sectionIndex 572 573 def reset(self): 574 ''' 575 reset prior to iteration 576 ''' 577 self.index = 0 578 self.iterSection = '_elements' 579 self.updateActiveInformation() 580 for f in self.filters: 581 if hasattr(f, 'reset'): 582 f.reset() 583 584 def resetCaches(self): 585 ''' 586 reset any cached data. -- do not use this at 587 the start of iteration since we might as well 588 save this information. But do call it if 589 the filter changes. 590 ''' 591 self._len = None 592 self._matchingElements = None 593 594 def cleanup(self): 595 ''' 596 stop iteration; and cleanup if need be. 597 ''' 598 if self.cleanupOnStop is not False: 599 self.reset() 600 601 del self.srcStream 602 del self.srcStreamElements 603 self.srcStream = None 604 self.srcStreamElements = () 605 606 # --------------------------------------------------------------- 607 # getting items 608 609 def matchingElements(self, *, restoreActiveSites: bool = True): 610 ''' 611 returns a list of elements that match the filter. 612 613 This sort of defeats the point of using a generator, so only used if 614 it's requested by __len__ or __getitem__ etc. 615 616 Subclasses should override to cache anything they need saved (index, 617 recursion objects, etc.) 618 619 activeSite will not be set. 620 621 Cached for speed. 622 623 >>> s = converter.parse('tinynotation: 3/4 c4 d e f g a', makeNotation=False) 624 >>> s.id = 'tn3/4' 625 >>> sI = s.iter() 626 >>> sI 627 <music21.stream.iterator.StreamIterator for Part:tn3/4 @:0> 628 629 >>> sI.matchingElements() 630 [<music21.meter.TimeSignature 3/4>, <music21.note.Note C>, <music21.note.Note D>, 631 <music21.note.Note E>, <music21.note.Note F>, <music21.note.Note G>, 632 <music21.note.Note A>] 633 634 >>> sI_notes = sI.notes 635 >>> sI_notes 636 <music21.stream.iterator.StreamIterator for Part:tn3/4 @:0> 637 638 Adding a filter to the Stream iterator returns a new Stream iterator; it 639 does not change the original. 640 641 >>> sI_notes is sI 642 False 643 644 >>> sI.filters 645 [] 646 647 >>> sI_notes.filters 648 [<music21.stream.filters.ClassFilter NotRest>] 649 650 >>> sI_notes.matchingElements() 651 [<music21.note.Note C>, <music21.note.Note D>, 652 <music21.note.Note E>, <music21.note.Note F>, <music21.note.Note G>, 653 <music21.note.Note A>] 654 655 If restoreActiveSites is False then the elements will not have 656 their activeSites changed (callers should use it when they do not plan to actually 657 expose the elements to users, such as in `__len__`). 658 659 Added in v7. -- restoreActiveSites 660 ''' 661 if self._matchingElements is not None: 662 return self._matchingElements 663 664 with saveAttributes(self, 'restoreActiveSites', 'index'): 665 self.restoreActiveSites = restoreActiveSites 666 me = [x for x in self] # pylint: disable=unnecessary-comprehension 667 self.reset() 668 669 if restoreActiveSites == self.restoreActiveSites: 670 # cache, if we are using the iterator default. 671 self._matchingElements = me 672 673 return me 674 675 def matchesFilters(self, e): 676 ''' 677 returns False if any filter returns False, True otherwise. 678 ''' 679 for f in self.filters: 680 f: Union[Callable, filters.StreamFilter] 681 try: 682 try: 683 if f(e, self) is False: 684 return False 685 except TypeError: # one element filters are acceptable. 686 f: Callable 687 if f(e) is False: 688 return False 689 except StopIteration: # pylint: disable=try-except-raise 690 raise # clearer this way to see that this can happen... 691 return True 692 693 def _newBaseStream(self): 694 ''' 695 Returns a new stream.Stream. The same thing as calling: 696 697 >>> s = stream.Stream() 698 699 So why does this exist? Since we can't import "music21.stream" here, 700 we will look in `srcStream.__class__.mro()` for the Stream 701 object to import. 702 703 This is used in places where returnStreamSubclass is False, so we 704 cannot just call `type(StreamIterator.srcStream)()` 705 706 >>> p = stream.Part() 707 >>> pi = p.iter() 708 >>> s = pi._newBaseStream() 709 >>> s 710 <music21.stream.Stream 0x1047eb2e8> 711 712 >>> pi.srcStream = note.Note() 713 >>> pi._newBaseStream() 714 Traceback (most recent call last): 715 music21.stream.iterator.StreamIteratorException: ... 716 ''' 717 StreamBase = None 718 for x in self.srcStream.__class__.mro(): 719 if x.__name__ == 'Stream': 720 StreamBase = x 721 break 722 723 try: 724 return StreamBase() 725 except TypeError: # 'NoneType' object is not callable. 726 raise StreamIteratorException( 727 f"You've given a 'stream' that is not a stream! {self.srcStream}") 728 729 def stream(self, returnStreamSubClass=True): 730 ''' 731 return a new stream from this iterator. 732 733 Does nothing except copy if there are no filters, but a drop in 734 replacement for the old .getElementsByClass() etc. if it does. 735 736 In other words: 737 738 `s.getElementsByClass()` == `s.iter().getElementsByClass().stream()` 739 740 >>> s = stream.Part() 741 >>> s.insert(0, note.Note('C')) 742 >>> s.append(note.Rest()) 743 >>> s.append(note.Note('D')) 744 >>> b = bar.Barline() 745 >>> s.storeAtEnd(b) 746 747 >>> s2 = s.iter().getElementsByClass('Note').stream() 748 >>> s2.show('t') 749 {0.0} <music21.note.Note C> 750 {2.0} <music21.note.Note D> 751 >>> s2.derivation.method 752 'getElementsByClass' 753 >>> s2 754 <music21.stream.Part ...> 755 756 >>> s3 = s.iter().stream() 757 >>> s3.show('t') 758 {0.0} <music21.note.Note C> 759 {1.0} <music21.note.Rest quarter> 760 {2.0} <music21.note.Note D> 761 {3.0} <music21.bar.Barline type=regular> 762 763 >>> s3.elementOffset(b, returnSpecial=True) 764 <OffsetSpecial.AT_END> 765 766 >>> s4 = s.iter().getElementsByClass('Barline').stream() 767 >>> s4.show('t') 768 {0.0} <music21.bar.Barline type=regular> 769 770 771 Note that this routine can create Streams that have elements that the original 772 stream did not, in the case of recursion: 773 774 >>> bach = corpus.parse('bwv66.6') 775 >>> bn = bach.flatten()[30] 776 >>> bn 777 <music21.note.Note E> 778 779 >>> bn in bach 780 False 781 >>> bfn = bach.recurse().notes.stream() 782 >>> bn in bfn 783 True 784 >>> bn.getOffsetBySite(bfn) 785 2.0 786 >>> bn.getOffsetInHierarchy(bach) 787 2.0 788 789 OMIT_FROM_DOCS 790 791 >>> s4._endElements[0] is b 792 True 793 ''' 794 ss = self.srcStream 795 796 # if this stream was sorted, the resultant stream is sorted 797 clearIsSorted = False 798 799 if returnStreamSubClass is True: 800 try: 801 found = ss.__class__() 802 except TypeError: 803 found = self._newBaseStream() 804 else: 805 found = self._newBaseStream() 806 807 found.mergeAttributes(ss) 808 found.derivation.origin = ss 809 if self.overrideDerivation is not None: 810 found.derivation.method = self.overrideDerivation 811 else: 812 derivationMethods = [] 813 for f in self.filters: 814 if hasattr(f, 'derivationStr'): 815 dStr = f.derivationStr 816 else: 817 dStr = f.__name__ # function; lambda returns <lambda> 818 derivationMethods.append(dStr) 819 found.derivation.method = '.'.join(derivationMethods) 820 821 fe = self.matchingElements() 822 for e in fe: 823 try: 824 o = ss.elementOffset(e, returnSpecial=True) 825 except SitesException: 826 # this can happen in the case of, s.recurse().notes.stream() -- need to do new 827 # stream... 828 o = e.getOffsetInHierarchy(ss) 829 clearIsSorted = True # now the stream is probably not sorted... 830 831 if not isinstance(o, str): 832 found.coreInsert(o, e, ignoreSort=True) 833 else: 834 if o == OffsetSpecial.AT_END: 835 found.coreStoreAtEnd(e) 836 else: 837 # TODO: something different... 838 found.coreStoreAtEnd(e) 839 840 if fe: 841 found.coreElementsChanged(clearIsSorted=clearIsSorted) 842 843 return found 844 845 @property 846 def activeElementList(self): 847 ''' 848 returns the element list ('_elements' or '_endElements') 849 for the current activeInformation 850 ''' 851 return getattr(self.activeInformation['stream'], self.activeInformation['iterSection']) 852 853 # ------------------------------------------------------------ 854 855 def addFilter(self: _SIter, newFilter, *, returnClone=True) -> _SIter: 856 ''' 857 Return a new StreamIterator with an additional filter. 858 Also resets caches -- so do not add filters any other way. 859 860 If returnClone is False then adds without creating a new StreamIterator 861 862 Changed in v.6 -- Encourage creating new StreamIterators: change 863 default to return a new StreamIterator. 864 ''' 865 if returnClone: 866 out = self.clone() 867 else: 868 out = self 869 870 out.resetCaches() 871 for f in out.filters: 872 if newFilter == f: 873 return out 874 out.filters.append(newFilter) 875 876 return out 877 878 def removeFilter(self: _SIter, oldFilter, *, returnClone=True) -> _SIter: 879 ''' 880 Return a new StreamIterator where oldFilter is removed. 881 ''' 882 if returnClone: 883 out = self.clone() 884 else: 885 out = self 886 887 out.resetCaches() 888 if oldFilter in out.filters: 889 out.filters.pop(out.filters.index(oldFilter)) 890 891 return out 892 893 def getElementById(self, elementId): 894 ''' 895 Returns a single element (or None) that matches elementId. 896 897 If chaining filters, this should be the last one, as it returns an element 898 899 >>> s = stream.Stream(id='s1') 900 >>> s.append(note.Note('C')) 901 >>> r = note.Rest() 902 >>> r.id = 'restId' 903 >>> s.append(r) 904 >>> r2 = s.recurse().getElementById('restId') 905 >>> r2 is r 906 True 907 >>> r2.id 908 'restId' 909 ''' 910 out = self.addFilter(filters.IdFilter(elementId)) 911 for e in out: 912 return e 913 return None 914 915 def getElementsByClass(self, classFilterList, *, returnClone=True): 916 ''' 917 Add a filter to the Iterator to remove all elements 918 except those that match one 919 or more classes in the `classFilterList`. A single class 920 can also used for the `classFilterList` parameter instead of a List. 921 922 >>> s = stream.Stream(id='s1') 923 >>> s.append(note.Note('C')) 924 >>> r = note.Rest() 925 >>> s.append(r) 926 >>> s.append(note.Note('D')) 927 >>> for el in s.iter().getElementsByClass('Rest'): 928 ... print(el) 929 <music21.note.Rest quarter> 930 931 932 ActiveSite is restored... 933 934 >>> s2 = stream.Stream(id='s2') 935 >>> s2.insert(0, r) 936 >>> r.activeSite.id 937 's2' 938 939 >>> for el in s.iter().getElementsByClass('Rest'): 940 ... print(el.activeSite.id) 941 s1 942 943 944 Classes work in addition to strings... 945 946 >>> for el in s.iter().getElementsByClass(note.Rest): 947 ... print(el) 948 <music21.note.Rest quarter> 949 950 ''' 951 return self.addFilter(filters.ClassFilter(classFilterList), returnClone=returnClone) 952 953 def getElementsByQuerySelector(self, querySelector: str, *, returnClone=True): 954 ''' 955 First implementation of a query selector, similar to CSS QuerySelectors used in 956 HTML DOM: 957 958 * A leading `#` indicates the id of an element, so '#hello' will find elements 959 with `el.id=='hello'` (should only be one) 960 * A leading `.` indicates the group of an element, so '.high' will find elements 961 with `'high' in el.groups` 962 * Any other string is considered to be the type/class of the element. So `Note` 963 will find all Note elements. Can be fully qualified like `note.Note` 964 965 Eventually, more complex query selectors will be implemented. This is just a start. 966 967 Setting up an example: 968 969 >>> s = converter.parse('tinyNotation: 4/4 GG4 AA4 BB4 r4 C4 D4 E4 F4 r1') 970 >>> s[note.Note].last().id = 'last' 971 >>> for n in s[note.Note]: 972 ... if n.octave == 3: 973 ... n.groups.append('tenor') 974 975 >>> list(s.recurse().getElementsByQuerySelector('.tenor')) 976 [<music21.note.Note C>, 977 <music21.note.Note D>, 978 <music21.note.Note E>, 979 <music21.note.Note F>] 980 981 >>> list(s.recurse().getElementsByQuerySelector('Rest')) 982 [<music21.note.Rest quarter>, 983 <music21.note.Rest whole>] 984 985 Note that unlike with stream slices, the querySelector does not do anything special 986 for id searches. `.first()` will need to be called to find the element (if any) 987 988 >>> s.recurse().getElementsByQuerySelector('#last').first() 989 <music21.note.Note F> 990 991 New in v.7 992 ''' 993 if querySelector.startswith('#'): 994 return self.addFilter(filters.IdFilter(querySelector[1:]), returnClone=returnClone) 995 if querySelector.startswith('.'): 996 return self.addFilter(filters.GroupFilter(querySelector[1:]), returnClone=returnClone) 997 return self.addFilter(filters.ClassFilter(querySelector), returnClone=returnClone) 998 999 1000 def getElementsNotOfClass(self, classFilterList, *, returnClone=True): 1001 ''' 1002 Adds a filter, removing all Elements that do not 1003 match the one or more classes in the `classFilterList`. 1004 1005 In lieu of a list, a single class can be used as the `classFilterList` parameter. 1006 1007 >>> a = stream.Stream() 1008 >>> a.repeatInsert(note.Rest(), range(10)) 1009 >>> for x in range(4): 1010 ... n = note.Note('G#') 1011 ... n.offset = x * 3 1012 ... a.insert(n) 1013 >>> found = a.iter().getElementsNotOfClass(note.Note) 1014 >>> len(found) 1015 10 1016 >>> found = a.iter().getElementsNotOfClass('Rest') 1017 >>> len(found) 1018 4 1019 >>> found = a.iter().getElementsNotOfClass(['Note', 'Rest']) 1020 >>> len(found) 1021 0 1022 1023 >>> b = stream.Stream() 1024 >>> b.repeatInsert(note.Rest(), range(15)) 1025 >>> a.insert(b) 1026 1027 >>> found = a.recurse().getElementsNotOfClass([note.Rest, 'Stream']) 1028 >>> len(found) 1029 4 1030 >>> found = a.recurse().getElementsNotOfClass([note.Note, 'Stream']) 1031 >>> len(found) 1032 25 1033 ''' 1034 return self.addFilter(filters.ClassNotFilter(classFilterList), returnClone=returnClone) 1035 1036 def getElementsByGroup(self, groupFilterList, *, returnClone=True): 1037 ''' 1038 >>> n1 = note.Note('C') 1039 >>> n1.groups.append('trombone') 1040 >>> n2 = note.Note('D') 1041 >>> n2.groups.append('trombone') 1042 >>> n2.groups.append('tuba') 1043 >>> n3 = note.Note('E') 1044 >>> n3.groups.append('tuba') 1045 >>> s1 = stream.Stream() 1046 >>> s1.append(n1) 1047 >>> s1.append(n2) 1048 >>> s1.append(n3) 1049 1050 >>> tboneSubStream = s1.iter().getElementsByGroup('trombone') 1051 >>> for thisNote in tboneSubStream: 1052 ... print(thisNote.name) 1053 C 1054 D 1055 >>> tubaSubStream = s1.iter().getElementsByGroup('tuba') 1056 >>> for thisNote in tubaSubStream: 1057 ... print(thisNote.name) 1058 D 1059 E 1060 ''' 1061 return self.addFilter(filters.GroupFilter(groupFilterList), returnClone=returnClone) 1062 1063 def getElementsByOffset( 1064 self, 1065 offsetStart, 1066 offsetEnd=None, 1067 *, 1068 includeEndBoundary=True, 1069 mustFinishInSpan=False, 1070 mustBeginInSpan=True, 1071 includeElementsThatEndAtStart=True, 1072 stopAfterEnd=True, 1073 returnClone=True, 1074 ) -> 'StreamIterator': 1075 ''' 1076 Adds a filter keeping only Music21Objects that 1077 are found at a certain offset or within a certain 1078 offset time range (given the start and optional stop values). 1079 1080 There are several attributes that govern how this range is 1081 determined: 1082 1083 1084 If `mustFinishInSpan` is True then an event that begins 1085 between offsetStart and offsetEnd but which ends after offsetEnd 1086 will not be included. The default is False. 1087 1088 1089 For instance, a half note at offset 2.0 will be found in 1090 getElementsByOffset(1.5, 2.5) or getElementsByOffset(1.5, 2.5, 1091 mustFinishInSpan = False) but not by getElementsByOffset(1.5, 2.5, 1092 mustFinishInSpan = True). 1093 1094 The `includeEndBoundary` option determines if an element 1095 begun just at the offsetEnd should be included. For instance, 1096 the half note at offset 2.0 above would be found by 1097 getElementsByOffset(0, 2.0) or by getElementsByOffset(0, 2.0, 1098 includeEndBoundary = True) but not by getElementsByOffset(0, 2.0, 1099 includeEndBoundary = False). 1100 1101 Setting includeEndBoundary to False at the same time as 1102 mustFinishInSpan is set to True is probably NOT what you want to do 1103 unless you want to find things like clefs at the end of the region 1104 to display as courtesy clefs. 1105 1106 The `mustBeginInSpan` option determines whether notes or other 1107 objects that do not begin in the region but are still sounding 1108 at the beginning of the region are excluded. The default is 1109 True -- that is, these notes will not be included. 1110 For instance the half note at offset 2.0 from above would not be found by 1111 getElementsByOffset(3.0, 3.5) or getElementsByOffset(3.0, 3.5, 1112 mustBeginInSpan = True) but it would be found by 1113 getElementsByOffset(3.0, 3.5, mustBeginInSpan = False) 1114 1115 Setting includeElementsThatEndAtStart to False is useful for zeroLength 1116 searches that set mustBeginInSpan == False to not catch notes that were 1117 playing before the search but that end just before the end of the search type. 1118 See the code for allPlayingWhileSounding for a demonstration. 1119 1120 This chart, and the examples below, demonstrate the various 1121 features of getElementsByOffset. It is one of the most complex 1122 methods of music21 but also one of the most powerful, so it 1123 is worth learning at least the basics. 1124 1125 .. image:: images/getElementsByOffset.* 1126 :width: 600 1127 1128 >>> st1 = stream.Stream() 1129 >>> n0 = note.Note('C') 1130 >>> n0.duration.type = 'half' 1131 >>> n0.offset = 0 1132 >>> st1.insert(n0) 1133 >>> n2 = note.Note('D') 1134 >>> n2.duration.type = 'half' 1135 >>> n2.offset = 2 1136 >>> st1.insert(n2) 1137 >>> out1 = list(st1.iter().getElementsByOffset(2)) 1138 >>> len(out1) 1139 1 1140 >>> out1[0].step 1141 'D' 1142 >>> out2 = list(st1.iter().getElementsByOffset(1, 3)) 1143 >>> len(out2) 1144 1 1145 >>> out2[0].step 1146 'D' 1147 >>> out3 = list(st1.iter().getElementsByOffset(1, 3, mustFinishInSpan=True)) 1148 >>> len(out3) 1149 0 1150 >>> out4 = list(st1.iter().getElementsByOffset(1, 2)) 1151 >>> len(out4) 1152 1 1153 >>> out4[0].step 1154 'D' 1155 >>> out5 = list(st1.iter().getElementsByOffset(1, 2, includeEndBoundary=False)) 1156 >>> len(out5) 1157 0 1158 >>> out6 = list(st1.iter().getElementsByOffset(1, 2, includeEndBoundary=False, 1159 ... mustBeginInSpan=False)) 1160 >>> len(out6) 1161 1 1162 >>> out6[0].step 1163 'C' 1164 >>> out7 = list(st1.iter().getElementsByOffset(1, 3, mustBeginInSpan=False)) 1165 >>> len(out7) 1166 2 1167 >>> [el.step for el in out7] 1168 ['C', 'D'] 1169 1170 Note, that elements that end at the start offset are included if mustBeginInSpan is False 1171 1172 >>> out8 = list(st1.iter().getElementsByOffset(2, 4, mustBeginInSpan=False)) 1173 >>> len(out8) 1174 2 1175 >>> [el.step for el in out8] 1176 ['C', 'D'] 1177 1178 To change this behavior set includeElementsThatEndAtStart=False 1179 1180 >>> out9 = list(st1.iter().getElementsByOffset(2, 4, mustBeginInSpan=False, 1181 ... includeElementsThatEndAtStart=False)) 1182 >>> len(out9) 1183 1 1184 >>> [el.step for el in out9] 1185 ['D'] 1186 1187 >>> a = stream.Stream(id='a') 1188 >>> n = note.Note('G') 1189 >>> n.quarterLength = 0.5 1190 >>> a.repeatInsert(n, list(range(8))) 1191 >>> b = stream.Stream(id='b') 1192 >>> b.repeatInsert(a, [0, 3, 6]) 1193 >>> c = list(b.iter().getElementsByOffset(2, 6.9)) 1194 >>> len(c) 1195 2 1196 >>> c = list(b.flatten().iter().getElementsByOffset(2, 6.9)) 1197 >>> len(c) 1198 10 1199 1200 Testing multiple zero-length elements with mustBeginInSpan: 1201 1202 >>> c = clef.TrebleClef() 1203 >>> ts = meter.TimeSignature('4/4') 1204 >>> ks = key.KeySignature(2) 1205 >>> s = stream.Stream() 1206 >>> s.insert(0.0, c) 1207 >>> s.insert(0.0, ts) 1208 >>> s.insert(0.0, ks) 1209 >>> len(list(s.iter().getElementsByOffset(0.0, mustBeginInSpan=True))) 1210 3 1211 >>> len(list(s.iter().getElementsByOffset(0.0, mustBeginInSpan=False))) 1212 3 1213 1214 On a :class:`~music21.stream.iterator.RecursiveIterator`, 1215 `.getElementsByOffset(0.0)`, will get everything 1216 at the start of the piece, which is useful: 1217 1218 >>> bwv66 = corpus.parse('bwv66.6') 1219 >>> list(bwv66.recurse().getElementsByOffset(0.0)) 1220 [<music21.metadata.Metadata object at 0x10a32f490>, 1221 <music21.stream.Part Soprano>, 1222 <music21.instrument.Instrument 'P1: Soprano: Instrument 1'>, 1223 <music21.stream.Measure 0 offset=0.0>, 1224 <music21.clef.TrebleClef>, 1225 <music21.key.Key of f# minor>, 1226 <music21.meter.TimeSignature 4/4>, 1227 <music21.note.Note C#>, 1228 <music21.stream.Part Alto>, 1229 ... 1230 <music21.note.Note E>, 1231 <music21.stream.Part Tenor>, 1232 ...] 1233 1234 However, any other offset passed to `getElementsByOffset` on a 1235 `RecursiveIterator` without additional arguments, is unlikely to be useful, 1236 because the iterator ends as soon as it encounters an element 1237 with an offset beyond the `offsetEnd` point. For instance, 1238 calling `.getElementsByOffset(1.0).notes` on a :class:`~music21.stream.Part`, 1239 in bwv66.6 only gets the note that appears at offset 1.0 of a measure that begins 1240 or includes offset 1.0. 1241 (Fortunately, this piece begins with a one-beat pickup, so there is such a note): 1242 1243 >>> soprano = bwv66.parts['Soprano'] 1244 >>> for el in soprano.recurse().getElementsByOffset(1.0): 1245 ... print(el, el.offset, el.getOffsetInHierarchy(bwv66), el.activeSite) 1246 <music21.stream.Measure 1 offset=1.0> 1.0 1.0 <music21.stream.Part Soprano> 1247 <music21.note.Note B> 1.0 2.0 <music21.stream.Measure 1 offset=1.0> 1248 1249 1250 RecursiveIterators will probably want to use 1251 :meth:`~music21.stream.iterator.RecursiveIterator.getElementsByOffsetInHierarchy` 1252 instead. Or to get all elements with a particular local offset, such as everything 1253 on the third quarter note of a measure, use the `stopAfterEnd=False` keyword, 1254 which lets the iteration continue to search for elements even after encountering 1255 some within Streams whose offsets are greater than the end element. 1256 1257 >>> len(soprano.recurse().getElementsByOffset(2.0, stopAfterEnd=False)) 1258 9 1259 1260 Changed in v5.5 -- all arguments changing behavior are keyword only. 1261 Added in v6.5 -- `stopAfterEnd` keyword. 1262 1263 OMIT_FROM_DOCS 1264 1265 Same test as above, but with floats 1266 1267 >>> out1 = list(st1.iter().getElementsByOffset(2.0)) 1268 >>> len(out1) 1269 1 1270 >>> out1[0].step 1271 'D' 1272 >>> out2 = list(st1.iter().getElementsByOffset(1.0, 3.0)) 1273 >>> len(out2) 1274 1 1275 >>> out2[0].step 1276 'D' 1277 >>> out3 = list(st1.iter().getElementsByOffset(1.0, 3.0, mustFinishInSpan=True)) 1278 >>> len(out3) 1279 0 1280 >>> out3b = list(st1.iter().getElementsByOffset(0.0, 3.001, mustFinishInSpan=True)) 1281 >>> len(out3b) 1282 1 1283 >>> out3b[0].step 1284 'C' 1285 >>> out3b = list(st1.iter().getElementsByOffset(1.0, 3.001, mustFinishInSpan=True, 1286 ... mustBeginInSpan=False)) 1287 >>> len(out3b) 1288 1 1289 >>> out3b[0].step 1290 'C' 1291 1292 >>> out4 = list(st1.iter().getElementsByOffset(1.0, 2.0)) 1293 >>> len(out4) 1294 1 1295 >>> out4[0].step 1296 'D' 1297 >>> out5 = list(st1.iter().getElementsByOffset(1.0, 2.0, includeEndBoundary=False)) 1298 >>> len(out5) 1299 0 1300 >>> out6 = list(st1.iter().getElementsByOffset(1.0, 2.0, includeEndBoundary=False, 1301 ... mustBeginInSpan=False)) 1302 >>> len(out6) 1303 1 1304 >>> out6[0].step 1305 'C' 1306 >>> out7 = list(st1.iter().getElementsByOffset(1.0, 3.0, mustBeginInSpan=False)) 1307 >>> len(out7) 1308 2 1309 >>> [el.step for el in out7] 1310 ['C', 'D'] 1311 ''' 1312 return self.addFilter( 1313 filters.OffsetFilter( 1314 offsetStart, 1315 offsetEnd, 1316 includeEndBoundary=includeEndBoundary, 1317 mustFinishInSpan=mustFinishInSpan, 1318 mustBeginInSpan=mustBeginInSpan, 1319 includeElementsThatEndAtStart=includeElementsThatEndAtStart, 1320 stopAfterEnd=stopAfterEnd, 1321 ), 1322 returnClone=returnClone 1323 ) 1324 1325 # ------------------------------------------------------------ 1326 # properties -- historical... 1327 1328 @property 1329 def notes(self): 1330 ''' 1331 Returns all NotRest objects 1332 1333 (will sometime become simply Note and Chord objects...) 1334 1335 >>> s = stream.Stream() 1336 >>> s.append(note.Note('C')) 1337 >>> s.append(note.Rest()) 1338 >>> s.append(note.Note('D')) 1339 >>> for el in s.iter().notes: 1340 ... print(el) 1341 <music21.note.Note C> 1342 <music21.note.Note D> 1343 ''' 1344 return self.addFilter(filters.ClassFilter('NotRest')) 1345 1346 @property 1347 def notesAndRests(self): 1348 ''' 1349 Returns all GeneralNote objects 1350 1351 >>> s = stream.Stream() 1352 >>> s.append(meter.TimeSignature('4/4')) 1353 >>> s.append(note.Note('C')) 1354 >>> s.append(note.Rest()) 1355 >>> s.append(note.Note('D')) 1356 >>> for el in s.iter().notesAndRests: 1357 ... print(el) 1358 <music21.note.Note C> 1359 <music21.note.Rest quarter> 1360 <music21.note.Note D> 1361 1362 chained filters... (this makes no sense since notes is a subset of notesAndRests) 1363 1364 >>> for el in s.iter().notesAndRests.notes: 1365 ... print(el) 1366 <music21.note.Note C> 1367 <music21.note.Note D> 1368 ''' 1369 return self.addFilter(filters.ClassFilter('GeneralNote')) 1370 1371 @property 1372 def parts(self): 1373 ''' 1374 Adds a ClassFilter for Part objects 1375 ''' 1376 return self.addFilter(filters.ClassFilter('Part')) 1377 1378 @property 1379 def spanners(self): 1380 ''' 1381 Adds a ClassFilter for Spanner objects 1382 ''' 1383 return self.addFilter(filters.ClassFilter('Spanner')) 1384 1385 @property 1386 def variants(self): 1387 ''' 1388 Deprecated in version 7 1389 1390 Adds a ClassFilter for Variant 1391 ''' 1392 return self.addFilter(filters.ClassFilter('Variant')) 1393 1394 @property 1395 def voices(self): 1396 ''' 1397 Adds a ClassFilter for Voice objects 1398 ''' 1399 return self.addFilter(filters.ClassFilter('Voice')) 1400 1401 1402# ----------------------------------------------------------------------------- 1403class OffsetIterator(StreamIterator): 1404 ''' 1405 An iterator that with each iteration returns a list of elements 1406 that are at the same offset (or all at end) 1407 1408 >>> s = stream.Stream() 1409 >>> s.insert(0, note.Note('C')) 1410 >>> s.insert(0, note.Note('D')) 1411 >>> s.insert(1, note.Note('E')) 1412 >>> s.insert(2, note.Note('F')) 1413 >>> s.insert(2, note.Note('G')) 1414 >>> s.storeAtEnd(bar.Repeat('end')) 1415 >>> s.storeAtEnd(clef.TrebleClef()) 1416 1417 >>> oIter = stream.iterator.OffsetIterator(s) 1418 >>> for groupedElements in oIter: 1419 ... print(groupedElements) 1420 [<music21.note.Note C>, <music21.note.Note D>] 1421 [<music21.note.Note E>] 1422 [<music21.note.Note F>, <music21.note.Note G>] 1423 [<music21.bar.Repeat direction=end>, <music21.clef.TrebleClef>] 1424 1425 Does it work again? 1426 1427 >>> for groupedElements2 in oIter: 1428 ... print(groupedElements2) 1429 [<music21.note.Note C>, <music21.note.Note D>] 1430 [<music21.note.Note E>] 1431 [<music21.note.Note F>, <music21.note.Note G>] 1432 [<music21.bar.Repeat direction=end>, <music21.clef.TrebleClef>] 1433 1434 1435 >>> for groupedElements in oIter.notes: 1436 ... print(groupedElements) 1437 [<music21.note.Note C>, <music21.note.Note D>] 1438 [<music21.note.Note E>] 1439 [<music21.note.Note F>, <music21.note.Note G>] 1440 1441 >>> for groupedElements in stream.iterator.OffsetIterator(s).getElementsByClass('Clef'): 1442 ... print(groupedElements) 1443 [<music21.clef.TrebleClef>] 1444 ''' 1445 1446 def __init__(self, 1447 srcStream, 1448 *, 1449 filterList=None, 1450 restoreActiveSites=True, 1451 activeInformation=None, 1452 ignoreSorting=False 1453 ): 1454 super().__init__(srcStream, 1455 filterList=filterList, 1456 restoreActiveSites=restoreActiveSites, 1457 activeInformation=activeInformation, 1458 ignoreSorting=ignoreSorting, 1459 ) 1460 self.raiseStopIterationNext = False 1461 self.nextToYield = [] 1462 self.nextOffsetToYield = None 1463 1464 def __next__(self) -> List[base.Music21Object]: 1465 if self.raiseStopIterationNext: 1466 raise StopIteration 1467 1468 retElementList = None 1469 # make sure that cleanup is not called during the loop... 1470 try: 1471 if self.nextToYield: 1472 retElementList = self.nextToYield 1473 retElOffset = self.nextOffsetToYield 1474 else: 1475 retEl = super().__next__() 1476 retElOffset = self.srcStream.elementOffset(retEl) 1477 retElementList = [retEl] 1478 1479 while self.index <= self.streamLength: 1480 nextEl = super().__next__() 1481 nextElOffset = self.srcStream.elementOffset(nextEl) 1482 if nextElOffset == retElOffset: 1483 retElementList.append(nextEl) 1484 else: 1485 self.nextToYield = [nextEl] 1486 self.nextOffsetToYield = nextElOffset 1487 return retElementList 1488 1489 except StopIteration: 1490 if retElementList: 1491 self.raiseStopIterationNext = True 1492 return retElementList 1493 else: 1494 raise StopIteration 1495 1496 def reset(self): 1497 ''' 1498 runs before iteration 1499 ''' 1500 super().reset() 1501 self.nextToYield = [] 1502 self.nextOffsetToYield = None 1503 self.raiseStopIterationNext = False 1504 1505 1506# ----------------------------------------------------------------------------- 1507class RecursiveIterator(StreamIterator): 1508 ''' 1509 One of the most powerful iterators in music21. Generally not called 1510 directly, but created by being invoked on a stream with `Stream.recurse()` 1511 1512 >>> b = corpus.parse('bwv66.6') 1513 >>> ri = stream.iterator.RecursiveIterator(b, streamsOnly=True) 1514 >>> for x in ri: 1515 ... print(x) 1516 <music21.stream.Part Soprano> 1517 <music21.stream.Measure 0 offset=0.0> 1518 <music21.stream.Measure 1 offset=1.0> 1519 <music21.stream.Measure 2 offset=5.0> 1520 ... 1521 <music21.stream.Part Alto> 1522 <music21.stream.Measure 0 offset=0.0> 1523 ... 1524 <music21.stream.Part Tenor> 1525 ... 1526 <music21.stream.Part Bass> 1527 ... 1528 1529 But this is how you'll actually use it: 1530 1531 >>> for x in b.recurse(streamsOnly=True, includeSelf=True): 1532 ... print(x) 1533 <music21.stream.Score 0x10484fd68> 1534 <music21.stream.Part Soprano> 1535 <music21.stream.Measure 0 offset=0.0> 1536 <music21.stream.Measure 1 offset=1.0> 1537 <music21.stream.Measure 2 offset=5.0> 1538 ... 1539 <music21.stream.Part Alto> 1540 <music21.stream.Measure 0 offset=0.0> 1541 ... 1542 <music21.stream.Part Tenor> 1543 ... 1544 <music21.stream.Part Bass> 1545 ... 1546 1547 >>> hasExpressions = lambda el, i: True if (hasattr(el, 'expressions') 1548 ... and el.expressions) else False 1549 >>> expressive = b.recurse().addFilter(hasExpressions) 1550 >>> expressive 1551 <music21.stream.iterator.RecursiveIterator for Score:0x10487f550 @:0> 1552 1553 >>> for el in expressive: 1554 ... print(el, el.expressions) 1555 <music21.note.Note C#> [<music21.expressions.Fermata>] 1556 <music21.note.Note A> [<music21.expressions.Fermata>] 1557 <music21.note.Note F#> [<music21.expressions.Fermata>] 1558 <music21.note.Note C#> [<music21.expressions.Fermata>] 1559 <music21.note.Note G#> [<music21.expressions.Fermata>] 1560 <music21.note.Note F#> [<music21.expressions.Fermata>] 1561 1562 >>> len(expressive) 1563 6 1564 >>> expressive[-1].measureNumber 1565 9 1566 >>> bool(expressive) 1567 True 1568 ''' 1569 1570 def __init__(self, 1571 srcStream, 1572 *, 1573 filterList=None, 1574 restoreActiveSites=True, 1575 activeInformation=None, 1576 streamsOnly=False, 1577 includeSelf=False, 1578 ignoreSorting=False 1579 ): # , parentIterator=None): 1580 super().__init__(srcStream, 1581 filterList=filterList, 1582 restoreActiveSites=restoreActiveSites, 1583 activeInformation=activeInformation, 1584 ignoreSorting=ignoreSorting, 1585 ) 1586 if 'lastYielded' not in self.activeInformation: 1587 self.activeInformation['lastYielded'] = None 1588 1589 self.returnSelf = includeSelf 1590 self.includeSelf = includeSelf 1591 self.ignoreSorting = ignoreSorting 1592 1593 # within the list of parent/child recursive iterators, where does this start? 1594 self.iteratorStartOffsetInHierarchy = 0.0 1595 1596 if streamsOnly is True: 1597 self.filters.append(filters.ClassFilter('Stream')) 1598 self.childRecursiveIterator = None 1599 # not yet used. 1600 # self.parentIterator = None 1601 1602 def __next__(self) -> base.Music21Object: 1603 ''' 1604 Get the next element of the stream under iteration. 1605 1606 The same __iter__ as the superclass is used. 1607 ''' 1608 while self.index < self.streamLength: 1609 # wrap this in a while loop instead of 1610 # returning self.__next__() because 1611 # in a long score with a miserly filter 1612 # it is possible to exceed maximum recursion 1613 # depth 1614 if self.childRecursiveIterator is not None: 1615 try: 1616 return next(self.childRecursiveIterator) 1617 except StopIteration: 1618 # self.childRecursiveIterator.parentIterator = None 1619 self.childRecursiveIterator = None 1620 1621 if self.returnSelf is True and self.matchesFilters(self.srcStream): 1622 self.activeInformation['stream'] = None 1623 self.activeInformation['index'] = -1 1624 self.activeInformation['lastYielded'] = self.srcStream 1625 self.returnSelf = False 1626 return self.srcStream 1627 1628 elif self.returnSelf is True: 1629 self.returnSelf = False 1630 1631 if self.index >= self.elementsLength: 1632 self.iterSection = '_endElements' 1633 self.sectionIndex = self.index - self.elementsLength 1634 else: 1635 self.sectionIndex = self.index 1636 1637 try: 1638 e = self.srcStreamElements[self.index] 1639 except IndexError: 1640 self.index += 1 1641 # this may happen if the number of elements has changed 1642 continue 1643 1644 self.index += 1 1645 1646 # in a recursive filter, the stream does not need to match the filter, 1647 # only the internal elements. 1648 if e.isStream: 1649 self.childRecursiveIterator = RecursiveIterator( 1650 srcStream=e, 1651 restoreActiveSites=self.restoreActiveSites, 1652 filterList=self.filters, # shared list... 1653 activeInformation=self.activeInformation, # shared dict 1654 includeSelf=False, # always for inner streams 1655 ignoreSorting=self.ignoreSorting, 1656 # parentIterator=self, 1657 ) 1658 newStartOffset = (self.iteratorStartOffsetInHierarchy 1659 + self.srcStream.elementOffset(e)) 1660 self.childRecursiveIterator.iteratorStartOffsetInHierarchy = newStartOffset 1661 if self.matchesFilters(e) is False: 1662 continue 1663 1664 if self.restoreActiveSites is True: 1665 self.srcStream.coreSelfActiveSite(e) 1666 1667 self.updateActiveInformation() 1668 self.activeInformation['lastYielded'] = e 1669 return e 1670 1671 # the last element can still set a recursive iterator, so make sure we handle it. 1672 if self.childRecursiveIterator is not None: 1673 try: 1674 return next(self.childRecursiveIterator) 1675 except StopIteration: 1676 # self.childRecursiveIterator.parentIterator = None 1677 self.childRecursiveIterator = None 1678 1679 self.activeInformation['lastYielded'] = None # always clean this up, no matter what... 1680 self.cleanup() 1681 raise StopIteration 1682 1683 def reset(self): 1684 ''' 1685 reset prior to iteration 1686 ''' 1687 self.returnSelf = self.includeSelf 1688 self.childRecursiveIterator = None 1689 self.activeInformation['lastYielded'] = None 1690 super().reset() 1691 1692 def matchingElements(self, *, restoreActiveSites=True): 1693 # saved parent iterator later? 1694 # will this work in mid-iteration? Test, or do not expose till then. 1695 with tempAttribute(self, 'childRecursiveIterator'): 1696 fe = super().matchingElements(restoreActiveSites=restoreActiveSites) 1697 return fe 1698 1699 def iteratorStack(self): 1700 ''' 1701 Returns a stack of RecursiveIterators at this point in the iteration. Last is most recent. 1702 1703 >>> b = corpus.parse('bwv66.6') 1704 >>> bRecurse = b.recurse() 1705 >>> i = 0 1706 >>> for _ in bRecurse: 1707 ... i += 1 1708 ... if i > 12: 1709 ... break 1710 >>> bRecurse.iteratorStack() 1711 [<music21.stream.iterator.RecursiveIterator for Score:0x10475cdd8 @:2>, 1712 <music21.stream.iterator.RecursiveIterator for Part:Soprano @:3>, 1713 <music21.stream.iterator.RecursiveIterator for Measure:m.1 @:3>] 1714 ''' 1715 iterStack = [self] 1716 x = self 1717 while x.childRecursiveIterator is not None: 1718 x = x.childRecursiveIterator 1719 iterStack.append(x) 1720 return iterStack 1721 1722 def streamStack(self): 1723 ''' 1724 Returns a stack of Streams at this point. Last is most recent. 1725 1726 However, the current element may be the same as the last element in the stack 1727 1728 >>> b = corpus.parse('bwv66.6') 1729 >>> bRecurse = b.recurse() 1730 >>> i = 0 1731 >>> for x in bRecurse: 1732 ... i += 1 1733 ... if i > 12: 1734 ... break 1735 >>> bRecurse.streamStack() 1736 [<music21.stream.Score 0x1049a0710>, 1737 <music21.stream.Part Soprano>, 1738 <music21.stream.Measure 1 offset=1.0>] 1739 ''' 1740 return [i.srcStream for i in self.iteratorStack()] 1741 1742 def currentHierarchyOffset(self): 1743 ''' 1744 Called on the current iterator, returns the current offset in the hierarchy. 1745 Or None if we are not currently iterating. 1746 1747 >>> b = corpus.parse('bwv66.6') 1748 >>> bRecurse = b.recurse().notes 1749 >>> print(bRecurse.currentHierarchyOffset()) 1750 None 1751 >>> for n in bRecurse: 1752 ... print(n.measureNumber, bRecurse.currentHierarchyOffset(), n) 1753 0 0.0 <music21.note.Note C#> 1754 0 0.5 <music21.note.Note B> 1755 1 1.0 <music21.note.Note A> 1756 1 2.0 <music21.note.Note B> 1757 1 3.0 <music21.note.Note C#> 1758 1 4.0 <music21.note.Note E> 1759 2 5.0 <music21.note.Note C#> 1760 ... 1761 9 34.5 <music21.note.Note E#> 1762 9 35.0 <music21.note.Note F#> 1763 0 0.0 <music21.note.Note E> 1764 1 1.0 <music21.note.Note F#> 1765 ... 1766 1767 After iteration completes, the figure is reset to None: 1768 1769 >>> print(bRecurse.currentHierarchyOffset()) 1770 None 1771 1772 The offsets are with respect to the position inside the stream 1773 being iterated, so for instance, this will not change the output from above: 1774 1775 >>> o = stream.Opus() 1776 >>> o.insert(20.0, b) 1777 >>> bRecurse = b.recurse().notes 1778 >>> for n in bRecurse: 1779 ... print(n.measureNumber, bRecurse.currentHierarchyOffset(), n) 1780 0 0.0 <music21.note.Note C#> 1781 ... 1782 1783 But of course, this will add 20.0 to all numbers: 1784 1785 >>> oRecurse = o.recurse().notes 1786 >>> for n in oRecurse: 1787 ... print(n.measureNumber, oRecurse.currentHierarchyOffset(), n) 1788 0 20.0 <music21.note.Note C#> 1789 ... 1790 1791 New in v.4 1792 ''' 1793 lastYield = self.activeInformation['lastYielded'] 1794 if lastYield is None: 1795 return None 1796 1797 iteratorStack = self.iteratorStack() 1798 newestIterator = iteratorStack[-1] 1799 lastStream = newestIterator.srcStream 1800 lastStartOffset = newestIterator.iteratorStartOffsetInHierarchy 1801 1802 if lastYield is lastStream: 1803 return common.opFrac(lastStartOffset) 1804 else: 1805 return common.opFrac(lastStartOffset + lastStream.elementOffset(lastYield)) 1806 # will still return numbers even if _endElements 1807 1808 def getElementsByOffsetInHierarchy( 1809 self, 1810 offsetStart, 1811 offsetEnd=None, 1812 *, 1813 includeEndBoundary=True, 1814 mustFinishInSpan=False, 1815 mustBeginInSpan=True, 1816 includeElementsThatEndAtStart=True) -> 'StreamIterator': 1817 ''' 1818 Adds a filter keeping only Music21Objects that 1819 are found at a certain offset or within a certain 1820 offset time range (given the `offsetStart` and optional `offsetEnd` values) from 1821 the beginning of the hierarchy. 1822 1823 >>> b = corpus.parse('bwv66.6') 1824 >>> for n in b.recurse().getElementsByOffsetInHierarchy(8, 9.5).notes: 1825 ... print(n, 1826 ... n.getOffsetInHierarchy(b), 1827 ... n.measureNumber, 1828 ... n.getContextByClass('Part').id) 1829 <music21.note.Note C#> 8.0 2 Soprano 1830 <music21.note.Note A> 9.0 3 Soprano 1831 <music21.note.Note B> 9.5 3 Soprano 1832 <music21.note.Note G#> 8.0 2 Alto 1833 <music21.note.Note F#> 9.0 3 Alto 1834 <music21.note.Note G#> 9.5 3 Alto 1835 <music21.note.Note C#> 8.0 2 Tenor 1836 <music21.note.Note C#> 9.0 3 Tenor 1837 <music21.note.Note D> 9.5 3 Tenor 1838 <music21.note.Note E#> 8.0 2 Bass 1839 <music21.note.Note F#> 9.0 3 Bass 1840 <music21.note.Note B> 9.5 3 Bass 1841 1842 Changed in v5.5 -- all behavior changing options are keyword only. 1843 ''' 1844 f = filters.OffsetHierarchyFilter( 1845 offsetStart, 1846 offsetEnd, 1847 includeEndBoundary=includeEndBoundary, 1848 mustFinishInSpan=mustFinishInSpan, 1849 mustBeginInSpan=mustBeginInSpan, 1850 includeElementsThatEndAtStart=includeElementsThatEndAtStart) 1851 return self.addFilter(f) 1852 1853 1854class Test(unittest.TestCase): 1855 def testSimpleClone(self): 1856 from music21 import note 1857 from music21 import stream 1858 s = stream.Stream() 1859 r = note.Rest() 1860 n = note.Note() 1861 s.append([r, n]) 1862 all_s = list(s.iter()) 1863 self.assertEqual(len(all_s), 2) 1864 self.assertIs(all_s[0], r) 1865 self.assertIs(all_s[1], n) 1866 s_notes = list(s.iter().notes) 1867 self.assertEqual(len(s_notes), 1) 1868 self.assertIs(s_notes[0], n) 1869 1870 def testAddingFiltersMidIteration(self): 1871 from music21 import note 1872 from music21 import stream 1873 s = stream.Stream() 1874 r = note.Rest() 1875 n = note.Note() 1876 s.append([r, n]) 1877 sIter = s.iter() 1878 r0 = next(sIter) 1879 self.assertIs(r0, r) 1880 1881 # adding a filter gives a new StreamIterator that restarts at 0 1882 sIter2 = sIter.getElementsByClass('GeneralNote') # this filter does nothing here. 1883 obj0 = next(sIter2) 1884 self.assertIs(obj0, r) 1885 1886 # original StreamIterator should be at its original spot, so this should 1887 # move to next element 1888 n0 = next(sIter) 1889 self.assertIs(n0, n) 1890 1891 def testRecursiveActiveSites(self): 1892 from music21 import converter 1893 s = converter.parse('tinyNotation: 4/4 c1 c4 d=id2 e f') 1894 rec = s.recurse() 1895 n = rec.getElementById('id2') 1896 self.assertEqual(n.activeSite.number, 2) 1897 1898 def testCurrentHierarchyOffsetReset(self): 1899 from music21 import note 1900 from music21 import stream 1901 p = stream.Part() 1902 m = stream.Measure() 1903 m.append(note.Note('D')) 1904 m.append(note.Note('E')) 1905 p.insert(0, note.Note('C')) 1906 p.append(m) 1907 pRecurse = p.recurse(includeSelf=True) 1908 allOffsets = [] 1909 for unused in pRecurse: 1910 allOffsets.append(pRecurse.currentHierarchyOffset()) 1911 self.assertListEqual(allOffsets, [0.0, 0.0, 1.0, 1.0, 2.0]) 1912 currentOffset = pRecurse.currentHierarchyOffset() 1913 self.assertIsNone(currentOffset) 1914 1915 def testAddingFiltersMidRecursiveIteration(self): 1916 from music21 import note 1917 from music21 import stream 1918 from music21.stream.iterator import RecursiveIterator as ImportedRecursiveIterator 1919 m = stream.Measure() 1920 r = note.Rest() 1921 n = note.Note() 1922 m.append([r, n]) 1923 p = stream.Part() 1924 p.append(m) 1925 1926 sc = stream.Score() 1927 sc.append(p) 1928 1929 sIter = sc.recurse() 1930 p0 = next(sIter) 1931 self.assertIs(p0, p) 1932 1933 child = sIter.childRecursiveIterator 1934 self.assertIsInstance(child, ImportedRecursiveIterator) 1935 1936 1937 1938 1939_DOC_ORDER = [StreamIterator, RecursiveIterator, OffsetIterator] 1940 1941if __name__ == '__main__': 1942 import music21 1943 music21.mainTest(Test) # , runTest='testCurrentHierarchyOffsetReset') 1944