1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Name:         stream/core.py
4# Purpose:      mixin class for the core elements of Streams
5#
6# Authors:      Michael Scott Cuthbert
7#               Christopher Ariza
8#
9# Copyright:    Copyright © 2008-2015 Michael Scott Cuthbert and the music21 Project
10# License:      BSD, see license.txt
11# -----------------------------------------------------------------------------
12'''
13the Stream Core Mixin handles the core attributes of streams that
14should be thought of almost as private values and not used except
15by advanced programmers who need the highest speed in programming.
16
17Nothing here promises to be stable.  The music21 team can make
18any changes here for efficiency reasons while being considered
19backwards compatible so long as the public methods that call this
20remain stable.
21
22All functions here will eventually begin with `.core`.
23'''
24import copy
25from typing import List, Dict, Union, Tuple, Optional
26from fractions import Fraction
27import unittest
28
29from music21.base import Music21Object
30from music21.common.enums import OffsetSpecial
31from music21.common.numberTools import opFrac
32from music21 import spanner
33from music21 import tree
34from music21.exceptions21 import StreamException, ImmutableStreamException
35from music21.stream.iterator import StreamIterator
36
37# pylint: disable=attribute-defined-outside-init
38class StreamCoreMixin:
39    '''
40    Core aspects of a Stream's behavior.  Any of these can change at any time.
41    '''
42    def __init__(self):
43        # hugely important -- keeps track of where the _elements are
44        # the _offsetDict is a dictionary where id(element) is the
45        # index and the value is a tuple of offset and element.
46        # offsets can be floats, Fractions, or a member of the enum OffsetSpecial
47        self._offsetDict: Dict[int, Tuple[Union[float, Fraction, str], Music21Object]] = {}
48
49        # self._elements stores Music21Object objects.
50        self._elements: List[Music21Object] = []
51
52        # self._endElements stores Music21Objects found at
53        # the highestTime of this Stream.
54        self._endElements: List[Music21Object] = []
55
56        self.isSorted = True
57        # should isFlat become readonly?
58        self.isFlat = True  # does it have no embedded Streams
59
60        # someday...
61        # self._elementTree = tree.trees.ElementTree(source=self)
62
63    def coreInsert(
64        self,
65        offset: Union[float, Fraction],
66        element: Music21Object,
67        *,
68        ignoreSort=False,
69        setActiveSite=True
70    ):
71        '''
72        N.B. -- a "core" method, not to be used by general users.  Run .insert() instead.
73
74        A faster way of inserting elements that does no checks,
75        just insertion.
76
77        Only be used in contexts that we know we have a proper, single Music21Object.
78        Best for usage when taking objects in a known Stream and creating a new Stream
79
80        When using this method, the caller is responsible for calling Stream.coreElementsChanged
81        after all operations are completed.
82
83        Do not mix coreInsert with coreAppend operations.
84
85        Returns boolean if the Stream is now sorted.
86        '''
87        # environLocal.printDebug(['coreInsert', 'self', self,
88        #    'offset', offset, 'element', element])
89        # need to compare highest time before inserting the element in
90        # the elements list
91        storeSorted = False
92        if not ignoreSort:
93            # # if sorted and our insertion is > the highest time, then
94            # # are still inserted
95            # if self.isSorted is True and self.highestTime <= offset:
96            #     storeSorted = True
97            if self.isSorted is True:
98                ht = self.highestTime
99                if ht < offset:
100                    storeSorted = True
101                elif ht == offset:
102                    if not self._elements:
103                        storeSorted = True
104                    else:
105                        highestSortTuple = self._elements[-1].sortTuple()
106                        thisSortTuple = list(element.sortTuple())
107                        thisSortTuple[1] = offset
108                        thisSortTuple = tuple(thisSortTuple)
109
110                        if highestSortTuple < thisSortTuple:
111                            storeSorted = True
112
113        self.coreSetElementOffset(
114            element,
115            float(offset),  # why is this not opFrac?
116            addElement=True,
117            setActiveSite=setActiveSite
118        )
119        element.sites.add(self)
120        # need to explicitly set the activeSite of the element
121        # will be sorted later if necessary
122        self._elements.append(element)
123        # self._elementTree.insert(float(offset), element)
124        return storeSorted
125
126    def coreAppend(
127        self,
128        element: Music21Object,
129        *,
130        setActiveSite=True
131    ):
132        '''
133        N.B. -- a "core" method, not to be used by general users.  Run .append() instead.
134
135        Low level appending; like `coreInsert` does not error check,
136        determine elements changed, or similar operations.
137
138        When using this method, the caller is responsible for calling
139        Stream.coreElementsChanged after all operations are completed.
140        '''
141        # NOTE: this is not called by append, as that is optimized
142        # for looping multiple elements
143        ht = self.highestTime
144        self.coreSetElementOffset(element, ht, addElement=True)
145        element.sites.add(self)
146        # need to explicitly set the activeSite of the element
147        if setActiveSite:
148            self.coreSelfActiveSite(element)
149        self._elements.append(element)
150
151        # Make this faster
152        # self._elementTree.insert(self.highestTime, element)
153        # does not change sorted state
154        self._setHighestTime(ht + element.duration.quarterLength)
155    # --------------------------------------------------------------------------
156    # adding and editing Elements and Streams -- all need to call coreElementsChanged
157    # most will set isSorted to False
158
159    def coreSetElementOffset(
160        self,
161        element: Music21Object,
162        offset: Union[int, float, Fraction, str],
163        *,
164        addElement=False,
165        setActiveSite=True
166    ):
167        '''
168        Sets the Offset for an element, very quickly.
169        Caller is responsible for calling :meth:`~music21.stream.core.coreElementsChanged`
170        afterward.
171
172        >>> s = stream.Stream()
173        >>> s.id = 'Stream1'
174        >>> n = note.Note('B-4')
175        >>> s.insert(10, n)
176        >>> n.offset
177        10.0
178        >>> s.coreSetElementOffset(n, 20.0)
179        >>> n.offset
180        20.0
181        >>> n.getOffsetBySite(s)
182        20.0
183        '''
184        # Note: not documenting 'highestTime' is on purpose, since can only be done for
185        # elements already stored at end.  Infinite loop.
186        try:
187            offset = opFrac(offset)
188        except TypeError:
189            if offset not in OffsetSpecial:  # pragma: no cover
190                raise StreamException(f'Cannot set offset to {offset!r} for {element}')
191
192        idEl = id(element)
193        if not addElement and idEl not in self._offsetDict:
194            raise StreamException(
195                f'Cannot set the offset for element {element}, not in Stream {self}.')
196        self._offsetDict[idEl] = (offset, element)  # fast
197        if setActiveSite:
198            self.coreSelfActiveSite(element)
199
200    def coreElementsChanged(
201        self,
202        *,
203        updateIsFlat=True,
204        clearIsSorted=True,
205        memo=None,
206        keepIndex=False,
207    ):
208        '''
209        NB -- a "core" stream method that is not necessary for most users.
210
211        This method is called automatically any time the elements in the Stream are changed.
212        However, it may be called manually in case sites or other advanced features of an
213        element have been modified.  It was previously a private method and for most users
214        should still be treated as such.
215
216        The various arguments permit optimizing the clearing of cached data in situations
217        when completely dropping all cached data is excessive.
218
219        >>> a = stream.Stream()
220        >>> a.isFlat
221        True
222
223        Here we manipulate the private `._elements` storage (which generally shouldn't
224        be done) using coreAppend and thus need to call `.coreElementsChanged` directly.
225
226        >>> a.coreAppend(stream.Stream())
227        >>> a.isFlat  # this is wrong.
228        True
229
230        >>> a.coreElementsChanged()
231        >>> a.isFlat
232        False
233        '''
234        # experimental
235        if not self._mutable:
236            raise ImmutableStreamException(
237                'coreElementsChanged should not be triggered on an immutable stream'
238            )
239
240        if memo is None:
241            memo = []
242
243        if id(self) in memo:
244            return
245        memo.append(id(self))
246
247        # WHY??? THIS SEEMS OVERKILL, esp. since the first call to .sort() in .flatten() will
248        # invalidate it! TODO: Investigate if this is necessary and then remove if not necessary
249        # should not need to do this...
250
251        # if this Stream is a flat representation of something, and its
252        # elements have changed, than we must clear the cache of that
253        # ancestor so that subsequent calls get a new representation of this derivation;
254        # we can do that by calling coreElementsChanged on
255        # the derivation.origin
256        if self._derivation is not None:
257            sdm = self._derivation.method
258            if sdm in ('flat', 'semiflat'):
259                origin: 'music21.stream.Stream' = self._derivation.origin
260                origin.clearCache()
261
262        # may not always need to clear cache of all living sites, but may
263        # always be a good idea since .flatten() has changed etc.
264        # should not need to do derivation.origin sites.
265        for livingSite in self.sites:
266            livingSite.coreElementsChanged(memo=memo)
267
268        # clear these attributes for setting later
269        if clearIsSorted:
270            self.isSorted = False
271
272        if updateIsFlat:
273            self.isFlat = True
274            # do not need to look in _endElements
275            for e in self._elements:
276                # only need to find one case, and if so, no longer flat
277                # fastest method here is isinstance()
278                # if isinstance(e, Stream):
279                if e.isStream:
280                    self.isFlat = False
281                    break
282        # resetting the cache removes lowest and highest time storage
283        # a slight performance optimization: not creating unless needed
284        if self._cache:
285            indexCache = None
286            if keepIndex and 'index' in self._cache:
287                indexCache = self._cache['index']
288            # always clear cache when elements have changed
289            # for instance, Duration will change.
290            # noinspection PyAttributeOutsideInit
291            self._cache = {}  # cannot call clearCache() because defined on Stream via Music21Object
292            if keepIndex and indexCache is not None:
293                self._cache['index'] = indexCache
294
295    def coreCopyAsDerivation(self, methodName: str, *, recurse=True, deep=True):
296        '''
297        Make a copy of this stream with the proper derivation set.
298
299        >>> s = stream.Stream()
300        >>> n = note.Note()
301        >>> s.append(n)
302        >>> s2 = s.coreCopyAsDerivation('exampleCopy')
303        >>> s2.derivation.method
304        'exampleCopy'
305        >>> s2.derivation.origin is s
306        True
307        >>> s2[0].derivation.method
308        'exampleCopy'
309        '''
310        if deep:
311            post = copy.deepcopy(self)
312        else:  # pragma: no cover
313            post = copy.copy(self)
314        post.derivation.method = methodName
315        if recurse and deep:
316            post.setDerivationMethod(methodName, recurse=True)
317        return post
318
319    def coreHasElementByMemoryLocation(self, objId: int) -> bool:
320        '''
321        NB -- a "core" stream method that is not necessary for most users. use hasElement(obj)
322
323        Return True if an element object id, provided as an argument, is contained in this Stream.
324
325        >>> s = stream.Stream()
326        >>> n1 = note.Note('g')
327        >>> n2 = note.Note('g#')
328        >>> s.append(n1)
329        >>> s.coreHasElementByMemoryLocation(id(n1))
330        True
331        >>> s.coreHasElementByMemoryLocation(id(n2))
332        False
333        '''
334        if objId in self._offsetDict:
335            return True
336
337        for e in self._elements:
338            if id(e) == objId:  # pragma: no cover
339                return True
340        for e in self._endElements:
341            if id(e) == objId:  # pragma: no cover
342                return True
343        return False
344
345    def coreGetElementByMemoryLocation(self, objId):
346        '''
347        NB -- a "core" stream method that is not necessary for most users.
348
349        Low-level tool to get an element based only on the object id.
350
351        This is not the same as getElementById, which refers to the id
352        attribute which may be manually set and not unique.
353
354        However, some implementations of python will reuse object locations, sometimes
355        quickly, so don't keep these around.
356
357        Used by spanner and variant.
358
359        >>> s = stream.Stream()
360        >>> n1 = note.Note('g')
361        >>> n2 = note.Note('g#')
362        >>> s.append(n1)
363        >>> s.coreGetElementByMemoryLocation(id(n1)) is n1
364        True
365        >>> s.coreGetElementByMemoryLocation(id(n2)) is None
366        True
367        >>> b = bar.Barline()
368        >>> s.storeAtEnd(b)
369        >>> s.coreGetElementByMemoryLocation(id(b)) is b
370        True
371        '''
372        # NOTE: this may be slightly faster than other approaches
373        # as it does not sort.
374        for e in self._elements:
375            if id(e) == objId:
376                return e
377        for e in self._endElements:
378            if id(e) == objId:
379                return e
380        return None
381
382    # --------------------------------------------------------------------------
383    def coreGuardBeforeAddElement(self, element, *, checkRedundancy=True):
384        '''
385        Before adding an element, this method performs
386        important checks on that element.
387
388        Used by:
389
390          - :meth:`~music21.stream.Stream.insert`
391          - :meth:`~music21.stream.Stream.append`
392          - :meth:`~music21.stream.Stream.storeAtEnd`
393          - `Stream.__init__()`
394
395        Returns None or raises a StreamException
396
397        >>> s = stream.Stream()
398        >>> s.coreGuardBeforeAddElement(s)
399        Traceback (most recent call last):
400        music21.exceptions21.StreamException: this Stream cannot be contained within itself
401
402        >>> s.append(s.iter())
403        Traceback (most recent call last):
404        music21.exceptions21.StreamException: cannot insert StreamIterator into a Stream
405        Iterate over it instead (User's Guide chs. 6 and 26)
406
407        >>> s.insert(4, 3.14159)
408        Traceback (most recent call last):
409        music21.exceptions21.StreamException: to put a non Music21Object in a stream,
410        create a music21.ElementWrapper for the item
411        '''
412        if element is self:  # cannot add this Stream into itself
413            raise StreamException('this Stream cannot be contained within itself')
414        if not isinstance(element, Music21Object):
415            if isinstance(element, StreamIterator):
416                raise StreamException('cannot insert StreamIterator into a Stream\n'
417                    "Iterate over it instead (User's Guide chs. 6 and 26)")
418            raise StreamException('to put a non Music21Object in a stream, '
419                                  'create a music21.ElementWrapper for the item')
420        if checkRedundancy:
421            # using id() here b/c we do not want to get __eq__ comparisons
422            idElement = id(element)
423            if idElement in self._offsetDict:
424                # now go slow for safety -- maybe something is amiss in the index.
425                # this should not happen, but we have slipped many times in not clearing out
426                # old _offsetDict entries.
427                for search_place in (self._elements, self._endElements):
428                    for eInStream in search_place:
429                        if eInStream is element:
430                            raise StreamException(
431                                f'the object ({element!r}, id()={id(element)} '
432                                + f'is already found in this Stream ({self!r}, id()={id(self)})'
433                            )
434                # something was old... delete from _offsetDict
435                # environLocal.warn('stale object')
436                del self._offsetDict[idElement]  # pragma: no cover
437        # if we do not purge locations here, we may have ids() for
438        # Streams that no longer exist stored in the locations entry for element.
439        # Note that dead locations are also purged from .sites during
440        # all get() calls.
441        element.purgeLocations()
442
443    def coreStoreAtEnd(self, element, setActiveSite=True):
444        '''
445        NB -- this is a "core" method.  Use .storeAtEnd() instead.
446
447        Core method for adding end elements.
448        To be called by other methods.
449        '''
450        self.coreSetElementOffset(element, OffsetSpecial.AT_END, addElement=True)
451        element.sites.add(self)
452        # need to explicitly set the activeSite of the element
453        if setActiveSite:
454            self.coreSelfActiveSite(element)
455        # self._elements.append(element)
456        self._endElements.append(element)
457
458    @property
459    def spannerBundle(self):
460        '''
461        A low-level object for Spanner management. This is a read-only property.
462        '''
463        if 'spannerBundle' not in self._cache or self._cache['spannerBundle'] is None:
464            spanners = self.recurse(classFilter=(spanner.Spanner,), restoreActiveSites=False)
465            self._cache['spannerBundle'] = spanner.SpannerBundle(list(spanners))
466        return self._cache['spannerBundle']
467
468    def asTimespans(self, classList=None, flatten=True):
469        r'''
470        Convert stream to a :class:`~music21.tree.trees.TimespanTree` instance, a
471        highly optimized data structure for searching through elements and
472        offsets.
473
474        >>> score = tree.makeExampleScore()
475        >>> scoreTree = score.asTimespans()
476        >>> print(scoreTree)
477        <TimespanTree {20} (0.0 to 8.0) <music21.stream.Score exampleScore>>
478            <ElementTimespan (0.0 to 0.0) <music21.clef.BassClef>>
479            <ElementTimespan (0.0 to 0.0) <music21.meter.TimeSignature 2/4>>
480            <ElementTimespan (0.0 to 0.0) <music21.instrument.Instrument 'PartA: : '>>
481            <ElementTimespan (0.0 to 0.0) <music21.clef.BassClef>>
482            <ElementTimespan (0.0 to 0.0) <music21.meter.TimeSignature 2/4>>
483            <ElementTimespan (0.0 to 0.0) <music21.instrument.Instrument 'PartB: : '>>
484            <PitchedTimespan (0.0 to 1.0) <music21.note.Note C>>
485            <PitchedTimespan (0.0 to 2.0) <music21.note.Note C#>>
486            <PitchedTimespan (1.0 to 2.0) <music21.note.Note D>>
487            <PitchedTimespan (2.0 to 3.0) <music21.note.Note E>>
488            <PitchedTimespan (2.0 to 4.0) <music21.note.Note G#>>
489            <PitchedTimespan (3.0 to 4.0) <music21.note.Note F>>
490            <PitchedTimespan (4.0 to 5.0) <music21.note.Note G>>
491            <PitchedTimespan (4.0 to 6.0) <music21.note.Note E#>>
492            <PitchedTimespan (5.0 to 6.0) <music21.note.Note A>>
493            <PitchedTimespan (6.0 to 7.0) <music21.note.Note B>>
494            <PitchedTimespan (6.0 to 8.0) <music21.note.Note D#>>
495            <PitchedTimespan (7.0 to 8.0) <music21.note.Note C>>
496            <ElementTimespan (8.0 to 8.0) <music21.bar.Barline type=final>>
497            <ElementTimespan (8.0 to 8.0) <music21.bar.Barline type=final>>
498        '''
499        hashedAttributes = hash((tuple(classList or ()), flatten))
500        cacheKey = "timespanTree" + str(hashedAttributes)
501        if cacheKey not in self._cache or self._cache[cacheKey] is None:
502            hashedTimespanTree = tree.fromStream.asTimespans(self,
503                                                             flatten=flatten,
504                                                             classList=classList)
505            self._cache[cacheKey] = hashedTimespanTree
506        return self._cache[cacheKey]
507
508    def coreSelfActiveSite(self, el):
509        '''
510        Set the activeSite of el to be self.
511
512        Override for SpannerStorage, VariantStorage, which should never
513        become the activeSite
514        '''
515        el.activeSite = self
516
517    def asTree(self, flatten=False, classList=None, useTimespans=False, groupOffsets=False):
518        '''
519        Returns an elementTree of the score, using exact positioning.
520
521        See tree.fromStream.asTree() for more details.
522
523        >>> score = tree.makeExampleScore()
524        >>> scoreTree = score.asTree(flatten=True)
525        >>> scoreTree
526        <ElementTree {20} (0.0 <0.-25...> to 8.0) <music21.stream.Score exampleScore>>
527        '''
528        hashedAttributes = hash((tuple(classList or ()),
529                                  flatten,
530                                  useTimespans,
531                                  groupOffsets))
532        cacheKey = "elementTree" + str(hashedAttributes)
533        if cacheKey not in self._cache or self._cache[cacheKey] is None:
534            hashedElementTree = tree.fromStream.asTree(self,
535                                                       flatten=flatten,
536                                                       classList=classList,
537                                                       useTimespans=useTimespans,
538                                                       groupOffsets=groupOffsets)
539            self._cache[cacheKey] = hashedElementTree
540        return self._cache[cacheKey]
541
542    def coreGatherMissingSpanners(
543        self,
544        *,
545        recurse=True,
546        requireAllPresent=True,
547        insert=True,
548        constrainingSpannerBundle: Optional[spanner.SpannerBundle] = None
549    ) -> Optional[List[spanner.Spanner]]:
550        '''
551        find all spanners that are referenced by elements in the
552        (recursed if recurse=True) stream and either inserts them in the Stream
553        (if insert is True) or returns them if insert is False.
554
555        If requireAllPresent is True (default) then only those spanners whose complete
556        spanned elements are in the Stream are returned.
557
558        Because spanners are stored weakly in .sites this is only guaranteed to find
559        the spanners in cases where the spanner is in another stream that is still active.
560
561        Here's a little helper function since we'll make the same Stream several times,
562        with two slurred notes, but without the slur itself.  Python's garbage collection
563        will get rid of the slur if we do not prevent it
564
565        >>> preventGarbageCollection = []
566        >>> def getStream():
567        ...    s = stream.Stream()
568        ...    n = note.Note('C')
569        ...    m = note.Note('D')
570        ...    sl = spanner.Slur(n, m)
571        ...    preventGarbageCollection.append(sl)
572        ...    s.append([n, m])
573        ...    return s
574
575        Okay now we have a Stream with two slurred notes, but without the slur.
576        `coreGatherMissingSpanners()` will put it in at the beginning.
577
578        >>> s = getStream()
579        >>> s.show('text')
580        {0.0} <music21.note.Note C>
581        {1.0} <music21.note.Note D>
582        >>> s.coreGatherMissingSpanners()
583        >>> s.show('text')
584        {0.0} <music21.note.Note C>
585        {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>>
586        {1.0} <music21.note.Note D>
587
588        Now, the same Stream, but insert is False, so it will return a list of
589        Spanners that should be inserted, rather than inserting them.
590
591        >>> s = getStream()
592        >>> spList = s.coreGatherMissingSpanners(insert=False)
593        >>> spList
594        [<music21.spanner.Slur <music21.note.Note C><music21.note.Note D>>]
595        >>> s.show('text')
596        {0.0} <music21.note.Note C>
597        {1.0} <music21.note.Note D>
598
599
600        Now we'll remove the second note so not all elements of the slur
601        are present, which by default will not insert the Slur:
602
603        >>> s = getStream()
604        >>> s.remove(s[-1])
605        >>> s.show('text')
606        {0.0} <music21.note.Note C>
607        >>> s.coreGatherMissingSpanners()
608        >>> s.show('text')
609        {0.0} <music21.note.Note C>
610
611        But with `requireAllPresent=False`, the spanner appears!
612
613        >>> s.coreGatherMissingSpanners(requireAllPresent=False)
614        >>> s.show('text')
615        {0.0} <music21.note.Note C>
616        {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>>
617
618        With `recurse=False`, then spanners are not gathered inside the inner
619        stream:
620
621        >>> t = stream.Part()
622        >>> s = getStream()
623        >>> t.insert(0, s)
624        >>> t.coreGatherMissingSpanners(recurse=False)
625        >>> t.show('text')
626        {0.0} <music21.stream.Stream 0x104935b00>
627            {0.0} <music21.note.Note C>
628            {1.0} <music21.note.Note D>
629
630
631        But the default acts with recursion:
632
633        >>> t.coreGatherMissingSpanners()
634        >>> t.show('text')
635        {0.0} <music21.stream.Stream 0x104935b00>
636            {0.0} <music21.note.Note C>
637            {1.0} <music21.note.Note D>
638        {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>>
639
640
641        Spanners already in the stream are not put there again:
642
643        >>> s = getStream()
644        >>> sl = s.notes.first().getSpannerSites()[0]
645        >>> sl
646        <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>>
647        >>> s.insert(0, sl)
648        >>> s.coreGatherMissingSpanners()
649        >>> s.show('text')
650        {0.0} <music21.note.Note C>
651        {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>>
652        {1.0} <music21.note.Note D>
653
654        Also does not happen with recursion.
655
656        >>> t = stream.Part()
657        >>> s = getStream()
658        >>> sl = s.notes.first().getSpannerSites()[0]
659        >>> s.insert(0, sl)
660        >>> t.insert(0, s)
661        >>> t.coreGatherMissingSpanners()
662        >>> t.show('text')
663        {0.0} <music21.stream.Stream 0x104935b00>
664            {0.0} <music21.note.Note C>
665            {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>>
666            {1.0} <music21.note.Note D>
667
668        If `constrainingSpannerBundle` is set then only spanners also present in
669        that spannerBundle are added.  This can be useful, for instance, in restoring
670        spanners from an excerpt that might already have spanners removed.  In
671        Jacob Tyler Walls's brilliant phrasing, it prevents regrowing zombie spanners
672        that you thought you had killed.
673
674        Here we will constrain only to spanners also present in another Stream:
675
676        >>> s = getStream()
677        >>> s2 = stream.Stream()
678        >>> s.coreGatherMissingSpanners(constrainingSpannerBundle=s2.spannerBundle)
679        >>> s.show('text')
680        {0.0} <music21.note.Note C>
681        {1.0} <music21.note.Note D>
682
683        Now with the same constraint, but we will put the Slur into the other stream.
684
685        >>> sl = s.notes.first().getSpannerSites()[0]
686        >>> s2.insert(0, sl)
687        >>> s.coreGatherMissingSpanners(constrainingSpannerBundle=s2.spannerBundle)
688        >>> s.show('text')
689        {0.0} <music21.note.Note C>
690        {0.0} <music21.spanner.Slur <music21.note.Note C><music21.note.Note D>>
691        {1.0} <music21.note.Note D>
692        '''
693        sb = self.spannerBundle
694        if recurse is True:
695            sIter = self.recurse()
696        else:
697            sIter = self.iter()
698
699        collectList = []
700        for el in list(sIter):
701            for sp in el.getSpannerSites():
702                if sp in sb:
703                    continue
704                if sp in collectList:
705                    continue
706                if constrainingSpannerBundle is not None and sp not in constrainingSpannerBundle:
707                    continue
708                if requireAllPresent:
709                    allFound = True
710                    for spannedElement in sp.getSpannedElements():
711                        if spannedElement not in sIter:
712                            allFound = False
713                            break
714                    if allFound is False:
715                        continue
716                collectList.append(sp)
717
718        if insert is False:
719            return collectList
720        elif collectList:  # do not run elementsChanged if nothing here.
721            for sp in collectList:
722                self.coreInsert(0, sp)
723            self.coreElementsChanged(updateIsFlat=False)
724
725# timing before: Macbook Air 2012, i7
726# In [3]: timeit('s = stream.Stream()', setup='from music21 import stream', number=100000)
727# Out[3]: 1.6291376419831067
728
729# after adding subclass -- actually faster, showing the rounding error:
730# In [2]: timeit('s = stream.Stream()', setup='from music21 import stream', number=100000)
731# Out[2]: 1.5247003990225494
732
733
734class Test(unittest.TestCase):
735    pass
736
737
738if __name__ == '__main__':
739    import music21
740    music21.mainTest(Test)
741