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

cuthbertLab / music21 / 15798035955

21 Jun 2025 05:30PM UTC coverage: 93.019% (+0.03%) from 92.99%
15798035955

Pull #1768

github

web-flow
Merge 23d36ad0d into 3516adf82
Pull Request #1768: More accurate spanner stop/start on import (and make import of exported MusicXML from those scores work, too)

213 of 215 new or added lines in 8 files covered. (99.07%)

149 existing lines in 2 files now uncovered.

81190 of 87283 relevant lines covered (93.02%)

0.93 hits per line

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

97.8
/music21/spanner.py
1
# -*- coding: utf-8 -*-
2
# ------------------------------------------------------------------------------
3
# Name:         spanner.py
4
# Purpose:      The Spanner base-class and subclasses
5
#
6
# Authors:      Christopher Ariza
7
#               Michael Scott Asato Cuthbert
8
#
9
# Copyright:    Copyright © 2010-2012 Michael Scott Asato Cuthbert
10
# License:      BSD, see license.txt
11
# ------------------------------------------------------------------------------
12
'''
13
A spanner is a music21 object that represents a connection usually between
14
two or more music21 objects that might live in different streams but need
15
some sort of connection between them.  A slur is one type of spanner -- it might
16
connect notes in different Measure objects or even between different parts.
17

18
This package defines some of the most common spanners.  Other spanners
19
can be found in modules such as :ref:`moduleDynamics` (for things such as crescendos).
20
'''
21
from __future__ import annotations
1✔
22

23
from collections.abc import Sequence, Iterable
1✔
24
import copy
1✔
25
import typing as t
1✔
26
import unittest
1✔
27

28
from music21 import base
1✔
29
from music21 import common
1✔
30
from music21.common.types import OffsetQL
1✔
31
from music21 import defaults
1✔
32
from music21 import environment
1✔
33
from music21 import exceptions21
1✔
34
from music21 import prebase
1✔
35
from music21 import sites
1✔
36
from music21 import style
1✔
37
if t.TYPE_CHECKING:
38
    from music21 import stream
39

40
environLocal = environment.Environment('spanner')
1✔
41

42

43
# ------------------------------------------------------------------------------
44
class SpannerException(exceptions21.Music21Exception):
1✔
45
    pass
1✔
46

47

48
class SpannerBundleException(exceptions21.Music21Exception):
1✔
49
    pass
1✔
50

51

52
# ------------------------------------------------------------------------------
53
class Spanner(base.Music21Object):
1✔
54
    # suppress this inspection because it fails when class is defined in
55
    # the __doc__
56
    # noinspection PyTypeChecker
57
    '''
58
    Spanner objects live on Streams in the same manner as other Music21Objects,
59
    but represent and store connections between one or more other Music21Objects.
60

61
    Commonly used Spanner subclasses include the :class:`~music21.spanner.Slur`,
62
    :class:`~music21.spanner.RepeatBracket`, :class:`~music21.spanner.Crescendo`,
63
    and :class:`~music21.spanner.Diminuendo`
64
    objects.
65

66
    In some cases you will want to subclass Spanner
67
    for specific purposes.
68

69
    In the first demo, we create
70
    a spanner to represent a written-out accelerando, such
71
    as Elliott Carter uses in his second string quartet (he marks them
72
    with an arrow).
73

74
    >>> class CarterAccelerandoSign(spanner.Spanner):
75
    ...    pass
76
    >>> n1 = note.Note('C4')
77
    >>> n2 = note.Note('D4')
78
    >>> n3 = note.Note('E4')
79
    >>> sp1 = CarterAccelerandoSign(n1, n2, n3)  # or as a list: [n1, n2, n3]
80
    >>> sp1.getSpannedElements()
81
    [<music21.note.Note C>, <music21.note.Note D>, <music21.note.Note E>]
82

83
    We can iterate over a spanner to get the contexts:
84

85
    >>> print(' '.join([repr(n) for n in sp1]))
86
    <music21.note.Note C> <music21.note.Note D> <music21.note.Note E>
87

88
    Now we put the notes and the spanner into a Stream object.  Note that
89
    the convention is to put the spanner at the beginning of the innermost
90
    Stream that contains all the Spanners:
91

92
    >>> s = stream.Stream()
93
    >>> s.append([n1, n2, n3])
94
    >>> s.insert(0, sp1)
95

96
    Now we can get at the spanner in one of three ways.
97

98
    (1) it is just a normal element in the stream:
99

100
    >>> for e in s:
101
    ...    print(e)
102
    <music21.note.Note C>
103
    <music21.CarterAccelerandoSign <music21.note.Note C><music21.note.Note D><music21.note.Note E>>
104
    <music21.note.Note D>
105
    <music21.note.Note E>
106

107
    (2) we can get a stream of spanners (equiv. to getElementsByClass(spanner.Spanner))
108
        by calling the .spanner property on the stream.
109

110
    >>> spannerCollection = s.spanners  # a stream object
111
    >>> for thisSpanner in spannerCollection:
112
    ...     print(thisSpanner)
113
    <music21.CarterAccelerandoSign <music21.note.Note C><music21.note.Note D><music21.note.Note E>>
114

115
    (3) we can get the spanner by looking at the list getSpannerSites() on
116
    any object that has a spanner:
117

118
    >>> n2.getSpannerSites()
119
    [<music21.CarterAccelerandoSign
120
            <music21.note.Note C><music21.note.Note D><music21.note.Note E>>]
121

122
    In this example we will slur a few notes and then iterate over the stream to
123
    see which are slurred:
124

125
    >>> n1 = note.Note('C4')
126
    >>> n2 = note.Note('D4')
127
    >>> n3 = note.Note('E4')
128
    >>> n4 = note.Note('F4')
129
    >>> n5 = note.Note('G4')
130
    >>> n6 = note.Note('A4')
131

132
    Create a slur over the second and third notes at instantiation:
133

134
    >>> slur1 = spanner.Slur([n2, n3])
135

136
    Slur the fifth and the sixth notes by adding them to an existing slur:
137

138
    >>> slur2 = spanner.Slur()
139
    >>> slur2.addSpannedElements([n5, n6])
140

141
    Now add them all to a stream:
142

143
    >>> part1 = stream.Part()
144
    >>> part1.append([n1, n2, n3, n4, n5, n6])
145
    >>> part1.insert(0, slur1)
146
    >>> part1.insert(0, slur2)
147

148
    Say we wanted to know which notes in a piece started a
149
    slur, here's how we could do it:
150

151
    >>> for n in part1.notes:
152
    ...    ss = n.getSpannerSites()
153
    ...    for thisSpanner in ss:
154
    ...       if 'Slur' in thisSpanner.classes:
155
    ...            if thisSpanner.isFirst(n):
156
    ...                print(n.nameWithOctave)
157
    D4
158
    G4
159

160
    Alternatively, you could iterate over the spanners
161
    of part1 and get their first elements:
162

163
    >>> for thisSpanner in part1.spanners:
164
    ...     firstNote = thisSpanner.getSpannedElements()[0]
165
    ...     print(firstNote.nameWithOctave)
166
    D4
167
    G4
168

169
    The second method is shorter, but the first is likely to
170
    be useful in cases where you are doing other things to
171
    each note object along the way.
172

173
    Oh, and of course, slurs do print properly in musicxml:
174

175
    >>> #_DOCS_SHOW part1.show()
176

177
    .. image:: images/slur1_example.*
178
        :width: 400
179

180
    (the Carter example would not print an arrow since that
181
    element has no corresponding musicxml representation).
182

183
    *Implementation notes:*
184

185
    The elements that are included in a spanner are stored in a
186
    Stream subclass called :class:`~music21.stream.SpannerStorage`
187
    found as the `.spannerStorage` attribute.  That Stream has an
188
    attribute called `client` which links to the original spanner.
189
    Thus, `spannerStorage` is smart enough to know where it's stored, but
190
    it makes deleting/garbage-collecting a spanner a tricky operation:
191

192
    Ex. Prove that the spannedElement Stream is linked to container via
193
    `client`:
194

195
    >>> sp1.spannerStorage.client is sp1
196
    True
197

198
    Spanners have a `.completeStatus` attribute which can be used to find out if
199
    all spanned elements have been added yet. It's up to the processing agent to
200
    set this, but it could be useful in deciding where to append a spanner.
201

202
    >>> sp1.completeStatus
203
    False
204

205
    When we're done adding elements:
206

207
    >>> sp1.completeStatus = True
208
    '''
209

210
    equalityAttributes = ('spannerStorage',)
1✔
211

212
    def __init__(self,
1✔
213
                 *spannedElements: t.Union[base.Music21Object,
214
                                           Sequence[base.Music21Object]],
215
                 **keywords):
216
        super().__init__(**keywords)
1✔
217

218
        # store a Stream inside of Spanner
219
        from music21 import stream
1✔
220

221
        # create a stream subclass, spanner storage; pass a reference
222
        # to this spanner for getting this spanner from the SpannerStorage
223
        # directly
224

225
        # TODO: Move here! along with VariantStorage to variant.
226
        self.spannerStorage = stream.SpannerStorage(client=self)
1✔
227

228
        # we do not want to auto sort based on offset or class, as
229
        # both are meaningless inside this Stream (and only have meaning
230
        # in Stream external to this)
231
        self.spannerStorage.autoSort = False
1✔
232

233
        # add arguments as a list or single item
234
        proc: list[base.Music21Object] = []
1✔
235
        for spannedElement in spannedElements:
1✔
236
            if isinstance(spannedElement, base.Music21Object):
1✔
237
                proc.append(spannedElement)
1✔
238
            elif spannedElement is not None:
1✔
239
                proc += spannedElement
1✔
240
        self.addSpannedElements(proc)
1✔
241

242
        # parameters that spanners need in loading and processing
243
        # local id is the id for the local area; used by musicxml
244
        self.idLocal: str|None = None
1✔
245
        # after all spannedElements have been gathered, setting complete
246
        # will mark that all parts have been gathered.
247
        self.completeStatus: bool = False
1✔
248

249
        # data for fill:
250

251
        # fillElementTypes is a list of types of object to search for.  This
252
        # can be set to something different in the __init__ of a particular
253
        # type of Spanner.
254
        # Set here to the empty list, so that by default, fill() does nothing.
255
        self.fillElementTypes: list[t.Type] = []
1✔
256

257
        # After a fill operation, filledStatus will be set to True.
258
        # Parsers and other clients can also set this to False or
259
        # True to mark whether or not a fill operation is needed
260
        # (False means fill is needed, True means fill is not
261
        # needed, presumably because the fill was done by hand).
262
        # Initialized to 'unknown'.
263
        self.filledStatus: bool|t.Literal['unknown'] = 'unknown'
1✔
264

265
    def _reprInternal(self):
1✔
266
        msg = []
1✔
267
        for c in self.getSpannedElements():
1✔
268
            objRef = c
1✔
269
            msg.append(repr(objRef))
1✔
270
        return ''.join(msg)
1✔
271

272
    def _deepcopySubclassable(self, memo=None, *, ignoreAttributes=None):
1✔
273
        '''
274
        see __deepcopy__ for tests and docs
275
        '''
276
        # NOTE: this is a performance critical operation
277
        defaultIgnoreSet = {'spannerStorage'}
1✔
278
        if ignoreAttributes is None:
1✔
279
            ignoreAttributes = defaultIgnoreSet
1✔
280
        else:
281
            ignoreAttributes = ignoreAttributes | defaultIgnoreSet
×
282
        new = t.cast(Spanner,
1✔
283
                     super()._deepcopySubclassable(memo, ignoreAttributes=ignoreAttributes))
284

285
        # we are temporarily putting in the PREVIOUS elements, to replace them later
286
        # with replaceSpannedElement()
287
        new.spannerStorage = type(self.spannerStorage)(client=new)
1✔
288
        for c in self.spannerStorage._elements:
1✔
289
            new.spannerStorage.coreAppend(c)
1✔
290
        # updateIsSorted too?
291
        new.spannerStorage.coreElementsChanged(updateIsFlat=False)
1✔
292
        return new
1✔
293

294
    def __deepcopy__(self, memo=None):
1✔
295
        '''
296
        This produces a new, independent object containing references
297
        to the same spannedElements.
298
        SpannedElements linked in this Spanner must be manually re-set,
299
        likely using the
300
        replaceSpannedElement() method.
301

302
        Notice that we put the references to the same object so that
303
        later we can replace them;
304
        otherwise in a deepcopy of a stream, the notes in the stream
305
        will become independent of the notes in the spanner.
306

307
        >>> import copy
308
        >>> n1 = note.Note('g')
309
        >>> n2 = note.Note('f#')
310
        >>> c1 = clef.AltoClef()
311

312
        >>> sp1 = spanner.Spanner(n1, n2, c1)
313
        >>> sp2 = copy.deepcopy(sp1)
314
        >>> len(sp2.spannerStorage)
315
        3
316
        >>> sp1 is sp2
317
        False
318
        >>> sp2[0] is sp1[0]
319
        True
320
        >>> sp2[2] is sp1[2]
321
        True
322
        >>> sp1[0] is n1
323
        True
324
        >>> sp2[0] is n1
325
        True
326
        '''
327
        return self._deepcopySubclassable(memo)
1✔
328

329
    # --------------------------------------------------------------------------
330
    # as spannedElements is private Stream, unwrap/wrap methods need to override
331
    # Music21Object to get at these objects
332
    # this is the same as with Variants
333

334
    def purgeOrphans(self, excludeStorageStreams=True):
1✔
335
        if self.spannerStorage:
1✔
336
            # might not be defined in the middle of a deepcopy.
337
            self.spannerStorage.purgeOrphans(excludeStorageStreams)
×
338
        base.Music21Object.purgeOrphans(self, excludeStorageStreams)
1✔
339

340
    def purgeLocations(self, rescanIsDead=False):
1✔
341
        # must override Music21Object to purge locations from the contained
342
        # Stream
343
        # base method to perform purge on the Stream
344
        if self.spannerStorage:
1✔
345
            # might not be defined in the middle of a deepcopy.
346
            self.spannerStorage.purgeLocations(rescanIsDead=rescanIsDead)
1✔
347
        base.Music21Object.purgeLocations(self, rescanIsDead=rescanIsDead)
1✔
348

349
    # --------------------------------------------------------------------------
350
    def __getitem__(self, key):
1✔
351
        '''
352

353
        >>> n1 = note.Note('g')
354
        >>> n2 = note.Note('f#')
355
        >>> c1 = clef.BassClef()
356
        >>> sl = spanner.Spanner(n1, n2, c1)
357
        >>> sl[0] == n1
358
        True
359
        >>> sl[-1] == c1
360
        True
361
        >>> sl[clef.BassClef][0] == c1
362
        True
363
        '''
364
        # delegate to Stream subclass
365
        return self.spannerStorage.__getitem__(key)
1✔
366

367
    def __iter__(self):
1✔
368
        return iter(self.spannerStorage)
1✔
369

370
    def __len__(self):
1✔
371
        # Check _elements to avoid StreamIterator overhead.
372
        # Safe, because impossible to put spanned elements at end.
373
        return len(self.spannerStorage._elements)
1✔
374

375
    def getSpannedElements(self):
1✔
376
        '''
377
        Return all the elements of `.spannerStorage` for this Spanner
378
        as a list of Music21Objects.
379

380

381
        >>> n1 = note.Note('g')
382
        >>> n2 = note.Note('f#')
383
        >>> sl = spanner.Spanner()
384
        >>> sl.addSpannedElements(n1)
385
        >>> sl.getSpannedElements() == [n1]
386
        True
387
        >>> sl.addSpannedElements(n2)
388
        >>> sl.getSpannedElements() == [n1, n2]
389
        True
390
        >>> sl.getSpannedElementIds() == [id(n1), id(n2)]
391
        True
392
        >>> c1 = clef.TrebleClef()
393
        >>> sl.addSpannedElements(c1)
394
        >>> sl.getSpannedElements() == [n1, n2, c1]  # make sure that not sorting
395
        True
396
        '''
397
        # Check _elements to avoid StreamIterator overhead.
398
        # Safe, because impossible to put spanned elements at end.
399
        return list(self.spannerStorage._elements)
1✔
400

401
    def getSpannedElementsByClass(self, classFilterList):
1✔
402
        '''
403

404
        >>> n1 = note.Note('g')
405
        >>> n2 = note.Note('f#')
406
        >>> c1 = clef.AltoClef()
407
        >>> sl = spanner.Spanner()
408
        >>> sl.addSpannedElements([n1, n2, c1])
409
        >>> sl.getSpannedElementsByClass('Note') == [n1, n2]
410
        True
411
        >>> sl.getSpannedElementsByClass(clef.Clef) == [c1]
412
        True
413
        '''
414
        # returns an iterator
415
        postStream = self.spannerStorage.getElementsByClass(classFilterList)
1✔
416
        # return raw elements list for speed
417
        return list(postStream)
1✔
418

419
    def getSpannedElementIds(self):
1✔
420
        '''
421
        Return all id() for all stored objects.
422
        Was performance critical, until most uses removed in v7.
423
        Used only as a testing tool now.
424
        Spanner.__contains__() was optimized in 839c7e5.
425
        '''
426
        return [id(n) for n in self.spannerStorage._elements]
1✔
427

428
    def addSpannedElements(
1✔
429
        self,
430
        spannedElements: t.Union[Sequence[base.Music21Object],
431
                                 base.Music21Object],
432
        *otherElements: base.Music21Object,
433
    ):
434
        '''
435
        Associate one or more elements with this Spanner.
436

437
        The order in which elements are added is retained and
438
        may or may not be significant to the spanner.
439

440
        >>> n1 = note.Note('g')
441
        >>> n2 = note.Note('f#')
442
        >>> n3 = note.Note('e')
443
        >>> n4 = note.Note('d-')
444
        >>> n5 = note.Note('c')
445

446
        >>> sl = spanner.Spanner()
447
        >>> sl.addSpannedElements(n1)
448
        >>> sl.addSpannedElements(n2, n3)
449
        >>> sl.addSpannedElements([n4, n5])
450
        >>> sl.getSpannedElementIds() == [id(n) for n in [n1, n2, n3, n4, n5]]
451
        True
452
        '''
453
        # presently, this does not look for redundancies
454
        # add mypy disables because isListLike() performs type-narrowing
455
        if not common.isListLike(spannedElements):
1✔
456
            spannedElements = [spannedElements]  # type: ignore[list-item]
1✔
457
        if otherElements:
1✔
458
            # copy
459
            spannedElements = spannedElements[:]  # type: ignore[index]
1✔
460
            # assume all other arguments are music21 objects
461
            spannedElements += otherElements  # type: ignore[operator]
1✔
462
        for c in spannedElements:  # type: ignore[union-attr]
1✔
463
            if c is None:
1✔
464
                continue
×
465
            if not self.hasSpannedElement(c):  # not already in storage
1✔
466
                self.spannerStorage.coreAppend(c)
1✔
467
            else:
468
                pass
1✔
469
                # it makes sense to not have multiple copies
470
                # environLocal.printDebug(['''attempting to add an object (%s) that is
471
                #    already found in the SpannerStorage stream of spanner %s;
472
                #    this may not be an error.''' % (c, self)])
473

474
        self.spannerStorage.coreElementsChanged()
1✔
475

476
    def insertFirstSpannedElement(self, firstEl: base.Music21Object):
1✔
477
        '''
478
        Add a single element as the first in the spanner.
479

480
        >>> n1 = note.Note('g')
481
        >>> n2 = note.Note('f#')
482
        >>> n3 = note.Note('e')
483
        >>> n4 = note.Note('d-')
484
        >>> n5 = note.Note('c')
485

486
        >>> sl = spanner.Spanner()
487
        >>> sl.addSpannedElements(n2, n3)
488
        >>> sl.addSpannedElements([n4, n5])
489
        >>> sl.insertFirstSpannedElement(n1)
490
        >>> sl.getSpannedElementIds() == [id(n) for n in [n1, n2, n3, n4, n5]]
491
        True
492
        '''
493
        origNumElements: int = len(self)
1✔
494
        self.addSpannedElements(firstEl)
1✔
495

496
        if origNumElements == 0:
1✔
497
            # no need to move to first element, it's already there
498
            return
1✔
499

500
        # now move it from last to first element (if it is not last element,
501
        # it was already in the spanner, and this API is a no-op).
502
        if self.spannerStorage.elements[-1] is firstEl:
1✔
503
            self.spannerStorage.elements = (
1✔
504
                (firstEl,) + self.spannerStorage.elements[:-1]
505
            )
506

507
    def hasSpannedElement(self, spannedElement: base.Music21Object) -> bool:
1✔
508
        '''
509
        Return True if this Spanner has the spannedElement.
510

511
        >>> n1 = note.Note('g')
512
        >>> n2 = note.Note('f#')
513
        >>> span = spanner.Spanner()
514
        >>> span.addSpannedElements(n1)
515
        >>> span.hasSpannedElement(n1)
516
        True
517
        >>> span.hasSpannedElement(n2)
518
        False
519

520
        Note that a simple `in` does the same thing:
521

522
        >>> n1 in span
523
        True
524
        >>> n2 in span
525
        False
526
        '''
527
        return spannedElement in self
1✔
528

529
    def __contains__(self, spannedElement):
1✔
530
        # Cannot check `in` spannerStorage._elements,
531
        # because it would check __eq__, not identity.
532
        for x in self.spannerStorage._elements:
1✔
533
            if x is spannedElement:
1✔
534
                return True
1✔
535
        return False
1✔
536

537
    def replaceSpannedElement(self, old, new) -> None:
1✔
538
        '''
539
        When copying a Spanner, we need to update the
540
        spanner with new references for copied  (if the Notes of a
541
        Slur have been copied, that Slur's Note references need
542
        references to the new Notes). Given the old spanned element,
543
        this method will replace the old with the new.
544

545
        The `old` parameter can be either an object or object id.
546

547
        >>> n1 = note.Note('g')
548
        >>> n2 = note.Note('f#')
549
        >>> c1 = clef.AltoClef()
550
        >>> c2 = clef.BassClef()
551
        >>> sl = spanner.Spanner(n1, n2, c1)
552
        >>> sl.replaceSpannedElement(c1, c2)
553
        >>> sl[-1] == c2
554
        True
555
        '''
556
        if old is None:
1✔
557
            return None  # do nothing
×
558
        if common.isNum(old):
1✔
559
            # this must be id(obj), not obj.id
560
            e = self.spannerStorage.coreGetElementByMemoryLocation(old)
×
561
            # e here is the old element that was spanned by this Spanner
562

563
            # environLocal.printDebug(['current Spanner.getSpannedElementIdsIds()',
564
            #    self.getSpannedElementIds()])
565
            # environLocal.printDebug(['Spanner.replaceSpannedElement:', 'getElementById result',
566
            #    e, 'old target', old])
567
            if e is not None:
×
568
                # environLocal.printDebug(['Spanner.replaceSpannedElement:', 'old', e, 'new', new])
569
                # do not do all Sites: only care about this one
570
                self.spannerStorage.replace(e, new, allDerived=False)
×
571
        else:
572
            # do not do all Sites: only care about this one
573
            self.spannerStorage.replace(old, new, allDerived=False)
1✔
574
            # environLocal.printDebug(['Spanner.replaceSpannedElement:', 'old', e, 'new', new])
575

576
        # while this Spanner now has proper elements in its spannerStorage Stream,
577
        # the element replaced likely has a site left-over from its previous Spanner
578

579
        # environLocal.printDebug(['replaceSpannedElement()', 'id(old)', id(old),
580
        #    'id(new)', id(new)])
581

582
    def fill(
1✔
583
        self,
584
        searchStream=None,  # stream.Stream|None, but cannot import stream here
585
        *,
586
        includeEndBoundary: bool = False,
587
        mustFinishInSpan: bool = False,
588
        mustBeginInSpan: bool = True,
589
        includeElementsThatEndAtStart: bool = False
590
    ):
591
        '''
592
        Fills in the intermediate elements of a spanner, that are found in searchStream between
593
        the first element's offset and the last element's offset+duration.  If searchStream
594
        is None, the first element's activeSite is used.  If the first element's activeSite
595
        is None, a SpannerException is raised.
596

597
        Ottava is an example of a Spanner that can be filled. The Ottava does not need
598
        to be inserted into the stream in order to be filled.
599

600
        >>> m = stream.Measure([note.Note('A'), note.Note('B'), note.Note('C')])
601
        >>> ott1 = spanner.Ottava(m.notes[0], m.notes[2])
602
        >>> ott1.fill(m)
603
        >>> ott1
604
        <music21.spanner.Ottava 8va transposing<...Note A><...Note B><...Note C>>
605

606
        If the searchStream is not passed in, fill still happens in this case, because
607
        the first note's activeSite is used instead.
608

609
        >>> ott2 = spanner.Ottava(m.notes[0], m.notes[2])
610
        >>> ott2.fill()
611
        >>> ott2
612
        <music21.spanner.Ottava 8va transposing<...Note A><...Note B><...Note C>>
613

614
        If the searchStream is not passed, and the spanner's first element doesn't have
615
        an activeSite, a SpannerException is raised.
616

617
        >>> ott3 = spanner.Ottava(note.Note('D'), note.Note('E'))
618
        >>> ott3.fill()
619
        Traceback (most recent call last):
620
        music21.spanner.SpannerException: Spanner.fill() requires a searchStream
621
            or getFirst().activeSite
622
        '''
623

624
        if not self.fillElementTypes:
1✔
625
            # nothing to fill
626
            return
1✔
627

628
        if self.filledStatus is True:
1✔
629
            # Don't fill twice.  If client wants a refill they can set filledStatus to False.
630
            return
1✔
631

632
        startElement: base.Music21Object|None = self.getFirst()
1✔
633
        if startElement is None:
1✔
634
            # no spanned elements?  Nothing to fill.
635
            return
1✔
636

637
        if searchStream is None:
1✔
638
            searchStream = startElement.activeSite
1✔
639
            if searchStream is None:
1✔
640
                raise SpannerException(
1✔
641
                    'Spanner.fill() requires a searchStream or getFirst().activeSite'
642
                )
643

644
        if t.TYPE_CHECKING:
645
            assert isinstance(searchStream, stream.Stream)
646

647
        endElement: base.Music21Object|None = self.getLast()
1✔
648
        if endElement is startElement:
1✔
649
            endElement = None
1✔
650

651
        savedEndElementOffset: OffsetQL | None = None
1✔
652
        savedEndElementActiveSite: stream.Stream | None = None
1✔
653
        if endElement is not None:
1✔
654
            # Start and end elements are different; we can't just append everything, we need
655
            # to save the end element, remove it, add everything, then add the end element
656
            # again.  Note that if there are actually more than 2 elements before we start
657
            # filling, the new intermediate elements will come after the existing ones,
658
            # regardless of offset.  But first and last will still be the same two elements
659
            # as before, which is the most important thing.
660

661
            # But doing this (remove/restore) clears endElement.offset and endElement.activeSite.
662
            # That's rude; put 'em back when we're done.
663
            savedEndElementOffset = endElement.offset
1✔
664
            savedEndElementActiveSite = endElement.activeSite
1✔
665
            self.spannerStorage.remove(endElement)
1✔
666

667
        try:
1✔
668
            startOffsetInHierarchy: OffsetQL = startElement.getOffsetInHierarchy(searchStream)
1✔
669
        except sites.SitesException:
1✔
670
            # print('start element not in searchStream')
671
            if endElement is not None:
1✔
672
                self.addSpannedElements(endElement)
1✔
673
                if savedEndElementOffset is not None:
1✔
674
                    endElement.offset = savedEndElementOffset
1✔
675
                if savedEndElementActiveSite is not None:
1✔
676
                    endElement.activeSite = savedEndElementActiveSite
1✔
677
            return
1✔
678

679
        endOffsetInHierarchy: OffsetQL
680
        if endElement is not None:
1✔
681
            try:
1✔
682
                endOffsetInHierarchy = (
1✔
683
                    endElement.getOffsetInHierarchy(searchStream) + endElement.quarterLength
684
                )
685
            except sites.SitesException:
1✔
686
                # print('end element not in searchStream')
687
                self.addSpannedElements(endElement)
1✔
688
                if savedEndElementOffset is not None:
1✔
689
                    endElement.offset = savedEndElementOffset
1✔
690
                if savedEndElementActiveSite is not None:
1✔
NEW
691
                    endElement.activeSite = savedEndElementActiveSite
×
692
                return
1✔
693
        else:
694
            endOffsetInHierarchy = (
1✔
695
                startOffsetInHierarchy + startElement.quarterLength
696
            )
697

698
        matchIterator = (searchStream
1✔
699
            .recurse()
700
            .getElementsByOffsetInHierarchy(
701
                startOffsetInHierarchy,
702
                endOffsetInHierarchy,
703
                includeEndBoundary=includeEndBoundary,
704
                mustFinishInSpan=mustFinishInSpan,
705
                mustBeginInSpan=mustBeginInSpan,
706
                includeElementsThatEndAtStart=includeElementsThatEndAtStart)
707
            .getElementsByClass(self.fillElementTypes)
708
        )
709

710
        for foundElement in matchIterator:
1✔
711
            if foundElement is startElement:
1✔
712
                # it's already in the spanner, skip it
713
                continue
1✔
714
            if foundElement is endElement:
1✔
715
                # we'll add it below, skip it
716
                continue
1✔
717
            self.addSpannedElements(foundElement)
1✔
718

719
        if endElement is not None:
1✔
720
            # add it back in as the end element
721
            self.addSpannedElements(endElement)
1✔
722
            if savedEndElementOffset is not None:
1✔
723
                endElement.offset = savedEndElementOffset
1✔
724
            if savedEndElementActiveSite is not None:
1✔
725
                endElement.activeSite = savedEndElementActiveSite
1✔
726

727
        self.filledStatus = True
1✔
728

729
    def isFirst(self, spannedElement):
1✔
730
        '''
731
        Given a spannedElement, is it first?
732

733
        >>> n1 = note.Note('g')
734
        >>> n2 = note.Note('f#')
735
        >>> n3 = note.Note('e')
736
        >>> n4 = note.Note('c')
737
        >>> n5 = note.Note('d-')
738

739
        >>> sl = spanner.Spanner()
740
        >>> sl.addSpannedElements(n1, n2, n3, n4, n5)
741
        >>> sl.isFirst(n2)
742
        False
743
        >>> sl.isFirst(n1)
744
        True
745
        >>> sl.isLast(n1)
746
        False
747
        >>> sl.isLast(n5)
748
        True
749
        '''
750
        return self.getFirst() is spannedElement
1✔
751

752
    def getFirst(self):
1✔
753
        '''
754
        Get the object of the first spannedElement (or None if it's an empty spanner)
755

756
        >>> n1 = note.Note('g')
757
        >>> n2 = note.Note('f#')
758
        >>> n3 = note.Note('e')
759
        >>> n4 = note.Note('c')
760
        >>> n5 = note.Note('d-')
761

762
        >>> sl = spanner.Spanner()
763
        >>> sl.addSpannedElements(n1, n2, n3, n4, n5)
764
        >>> sl.getFirst() is n1
765
        True
766

767
        >>> spanner.Slur().getFirst() is None
768
        True
769
        '''
770
        try:
1✔
771
            return self.spannerStorage[0]
1✔
772
        except (IndexError, exceptions21.StreamException):
1✔
773
            return None
1✔
774

775
    def isLast(self, spannedElement):
1✔
776
        '''
777
        Given a spannedElement, is it last?  Returns True or False
778
        '''
779
        return self.getLast() is spannedElement
1✔
780

781
    def getLast(self):
1✔
782
        '''
783
        Get the object of the last spannedElement (or None if it's an empty spanner)
784

785
        >>> n1 = note.Note('g')
786
        >>> n2 = note.Note('f#')
787
        >>> n3 = note.Note('e')
788
        >>> n4 = note.Note('c')
789
        >>> n5 = note.Note('d-')
790

791
        >>> sl = spanner.Spanner()
792
        >>> sl.addSpannedElements(n1, n2, n3, n4, n5)
793
        >>> sl.getLast() is n5
794
        True
795

796
        >>> spanner.Slur().getLast() is None
797
        True
798
        '''
799
        try:
1✔
800
            return self.spannerStorage[-1]
1✔
801
        except (IndexError, exceptions21.StreamException):
1✔
802
            return None
1✔
803

804

805
# ------------------------------------------------------------------------------
806
class PendingAssignmentRef(t.TypedDict):
1✔
807
    '''
808
    An object containing information about a pending first spanned element
809
    assignment. See setPendingFirstSpannedElementAssignment for documentation
810
    and tests.
811
    '''
812
    # noinspection PyTypedDict
813
    spanner: 'Spanner'
1✔
814
    className: str
1✔
815
    offsetInScore: OffsetQL|None
1✔
816
    clientInfo: t.Any|None
1✔
817

818
class SpannerAnchor(base.Music21Object):
1✔
819
    '''
820
    A simple Music21Object that can be used to define the beginning or end
821
    of a Spanner, in the place of a GeneralNote.
822

823
    This is useful for (e.g.) a Crescendo that ends partway through a
824
    note (e.g. in a violin part).  Exporters (like MusicXML) are configured
825
    to remove the SpannerAnchor itself on output, exporting only the Spanner
826
    start and stop locations.
827

828
    Here's an example of a whole note that has a Crescendo for the first
829
    half of the note, and a Diminuendo for the second half of the note.
830

831
    >>> n = note.Note('C4', quarterLength=4)
832
    >>> measure = stream.Measure([n], number=1)
833
    >>> part = stream.Part([measure], id='violin')
834
    >>> score = stream.Score([part])
835

836
    Add a crescendo from the note's start to the first anchor, place in the
837
    middle of the note, and then a diminuendo from that first anchor to the
838
    second, placed at the end of the note.
839

840
    >>> anchor1 = spanner.SpannerAnchor()
841
    >>> anchor2 = spanner.SpannerAnchor()
842
    >>> measure.insert(2.0, anchor1)
843
    >>> measure.insert(4.0, anchor2)
844
    >>> cresc = dynamics.Crescendo(n, anchor1)
845
    >>> dim = dynamics.Diminuendo(anchor1, anchor2)
846
    >>> score.append((cresc, dim))
847
    >>> score.show('text')
848
    {0.0} <music21.stream.Part violin>
849
        {0.0} <music21.stream.Measure 1 offset=0.0>
850
            {0.0} <music21.note.Note C>
851
            {2.0} <music21.spanner.SpannerAnchor at 2.0>
852
            {4.0} <music21.spanner.SpannerAnchor at 4.0>
853
    {4.0} <music21.dynamics.Crescendo <music21.note.Note C><...SpannerAnchor at 2.0>>
854
    {4.0} <music21.dynamics.Diminuendo <...SpannerAnchor at 2.0><...SpannerAnchor at 4.0>>
855
    '''
856
    def __init__(self, **keywords):
1✔
857
        super().__init__(**keywords)
1✔
858

859
    def _reprInternal(self) -> str:
1✔
860
        offset: OffsetQL = self.offset
1✔
861
        if self.activeSite is None:
1✔
862
            # find a site that is either a Measure or a Voice
863
            siteList: list = self.sites.getSitesByClass('Measure')
1✔
864
            if not siteList:
1✔
865
                siteList = self.sites.getSitesByClass('Voice')
1✔
866
            if not siteList:
1✔
867
                return 'unanchored'
1✔
868
            offset = self.getOffsetInHierarchy(siteList[0])
1✔
869

870
        ql: OffsetQL = self.duration.quarterLength
1✔
871
        if ql == 0:
1✔
872
            return f'at {offset}'
1✔
873

874
        return f'at {offset}-{offset + ql}'
1✔
875

876

877
class SpannerBundle(prebase.ProtoM21Object):
1✔
878
    '''
879
    An advanced utility object for collecting and processing
880
    collections of Spanner objects. This is necessary because
881
    often processing routines that happen at many
882
    levels still need access to the same collection of spanners.
883

884
    Because SpannerBundles are so commonly used with
885
    :class:`~music21.stream.Stream` objects, the Stream has a
886
    :attr:`~music21.stream.Stream.spannerBundle` property that stores
887
    and caches a SpannerBundle of the Stream.
888

889
    If a Stream or Stream subclass is provided as an argument,
890
    all Spanners on this Stream will be accumulated herein.
891

892
    Not to be confused with SpannerStorage (which is a Stream class inside
893
    a spanner that stores Elements that are spanned)
894

895
    * Changed in v7: only argument must be a List of spanners.
896
      Creators of SpannerBundles are required to check that this constraint is True
897
    '''
898
    # TODO: make SpannerBundle a Generic type
899
    def __init__(self, spanners: list[Spanner]|None = None):
1✔
900
        self._cache: dict[str, t.Any] = {}  # cache is defined on Music21Object not ProtoM21Object
1✔
901

902
        self._storage: list[Spanner] = []
1✔
903
        if spanners:
1✔
904
            self._storage = spanners[:]  # a simple List, not a Stream
1✔
905

906
        # special spanners, stored in storage, can be identified in the
907
        # SpannerBundle as missing a first spannedElement; the next obj that meets
908
        # the class expectation will then be assigned and the spannedElement
909
        # cleared
910
        self._pendingSpannedElementAssignment: list[PendingAssignmentRef] = []
1✔
911

912
    def append(self, other: Spanner):
1✔
913
        '''
914
        adds a Spanner to the bundle. Will be done automatically when adding a Spanner
915
        to a Stream.
916
        '''
917
        self._storage.append(other)
1✔
918
        self._cache.clear()
1✔
919

920
    def __len__(self):
1✔
921
        return len(self._storage)
1✔
922

923
    def __iter__(self):
1✔
924
        return self._storage.__iter__()
1✔
925

926
    def __getitem__(self, key) -> Spanner:
1✔
927
        return self._storage[key]
1✔
928

929
    def remove(self, item: Spanner):
1✔
930
        '''
931
        Remove a stored Spanner from the bundle with an instance.
932
        Each reference must have a matching id() value.
933

934
        >>> su1 = spanner.Slur()
935
        >>> su1.idLocal = 1
936
        >>> su2 = spanner.Slur()
937
        >>> su2.idLocal = 2
938
        >>> sb = spanner.SpannerBundle()
939
        >>> sb.append(su1)
940
        >>> sb.append(su2)
941
        >>> len(sb)
942
        2
943
        >>> sb
944
        <music21.spanner.SpannerBundle of size 2>
945

946
        >>> sb.remove(su2)
947
        >>> len(sb)
948
        1
949
        '''
950
        if item in self._storage:
1✔
951
            self._storage.remove(item)
1✔
952
        else:
953
            raise SpannerBundleException(f'cannot match object for removal: {item}')
×
954
        self._cache.clear()
1✔
955

956
    def _reprInternal(self):
1✔
957
        return f'of size {len(self)}'
1✔
958

959
    def getSpannerStorageIds(self) -> list[int]:
1✔
960
        '''
961
        Return all SpannerStorage ids from all contained Spanners
962
        '''
963
        post: list[int] = []
1✔
964
        for x in self._storage:
1✔
965
            post.append(id(x.spannerStorage))
1✔
966
        return post
1✔
967

968
    def getByIdLocal(self, idLocal: int|None = None) -> SpannerBundle:
1✔
969
        '''
970
        Get spanners by `idLocal`.
971

972
        Returns a new SpannerBundle object
973

974
        >>> su = spanner.Slur()
975
        >>> su.idLocal = 1
976
        >>> rb = spanner.RepeatBracket()
977
        >>> rb.idLocal = 2
978
        >>> sb = spanner.SpannerBundle()
979
        >>> sb.append(su)
980
        >>> sb.append(rb)
981
        >>> len(sb)
982
        2
983

984
        >>> sb.getByIdLocal(2)
985
        <music21.spanner.SpannerBundle of size 1>
986
        >>> sb.getByIdLocal(2)[0]
987
        <music21.spanner.RepeatBracket >
988

989
        >>> len(sb.getByIdLocal(1))
990
        1
991

992
        >>> sb.getByIdLocal(3)
993
        <music21.spanner.SpannerBundle of size 0>
994
        '''
995
        cacheKey = f'idLocal-{idLocal}'
1✔
996
        if cacheKey not in self._cache or self._cache[cacheKey] is None:
1✔
997
            out: list[Spanner] = []
1✔
998
            for sp in self._storage:
1✔
999
                if sp.idLocal == idLocal:
1✔
1000
                    out.append(sp)
1✔
1001
            self._cache[cacheKey] = self.__class__(out)
1✔
1002
        return self._cache[cacheKey]
1✔
1003

1004
    def getByCompleteStatus(self, completeStatus: bool) -> SpannerBundle:
1✔
1005
        '''
1006
        Get spanners by matching status of `completeStatus` to the same attribute
1007

1008
        >>> su1 = spanner.Slur()
1009
        >>> su1.idLocal = 1
1010
        >>> su1.completeStatus = True
1011
        >>> su2 = spanner.Slur()
1012
        >>> su2.idLocal = 2
1013
        >>> sb = spanner.SpannerBundle()
1014
        >>> sb.append(su1)
1015
        >>> sb.append(su2)
1016
        >>> sb2 = sb.getByCompleteStatus(True)
1017
        >>> len(sb2)
1018
        1
1019
        >>> sb2 = sb.getByIdLocal(1).getByCompleteStatus(True)
1020
        >>> sb2[0] == su1
1021
        True
1022
        '''
1023
        # cannot cache, as complete status may change internally
1024
        post: list[Spanner] = []
1✔
1025
        for sp in self._storage:
1✔
1026
            if sp.completeStatus == completeStatus:
1✔
1027
                post.append(sp)
1✔
1028
        return self.__class__(post)
1✔
1029

1030
    def getBySpannedElement(self, spannedElement: Spanner) -> SpannerBundle:
1✔
1031
        '''
1032
        Given a spanner spannedElement (an object),
1033
        return a new SpannerBundle of all Spanner objects that
1034
        have this object as a spannedElement.
1035

1036
        >>> n1 = note.Note()
1037
        >>> n2 = note.Note()
1038
        >>> n3 = note.Note()
1039
        >>> su1 = spanner.Slur(n1, n2)
1040
        >>> su2 = spanner.Slur(n2, n3)
1041
        >>> sb = spanner.SpannerBundle()
1042
        >>> sb.append(su1)
1043
        >>> sb.append(su2)
1044
        >>> list(sb.getBySpannedElement(n1)) == [su1]
1045
        True
1046
        >>> list(sb.getBySpannedElement(n2)) == [su1, su2]
1047
        True
1048
        >>> list(sb.getBySpannedElement(n3)) == [su2]
1049
        True
1050
        '''
1051
        # NOTE: this is a performance critical operation
1052

1053
        # idTarget = id(spannedElement)
1054
        # post = self.__class__()
1055
        # for sp in self._storage:  # storage is a list
1056
        #     if idTarget in sp.getSpannedElementIds():
1057
        #         post.append(sp)
1058
        # return post
1059

1060
        idTarget = id(spannedElement)
1✔
1061
        cacheKey = f'getBySpannedElement-{idTarget}'
1✔
1062
        if cacheKey not in self._cache or self._cache[cacheKey] is None:
1✔
1063
            out: list[Spanner] = []
1✔
1064
            for sp in self._storage:  # storage is a list of spanners
1✔
1065
                # __contains__() will test for identity, not equality
1066
                # see Spanner.hasSpannedElement(), which just calls __contains__()
1067
                if spannedElement in sp:
1✔
1068
                    out.append(sp)
1✔
1069
            self._cache[cacheKey] = self.__class__(out)
1✔
1070
        return self._cache[cacheKey]
1✔
1071

1072
    def replaceSpannedElement(
1✔
1073
        self,
1074
        old: base.Music21Object,
1075
        new: base.Music21Object
1076
    ) -> list[Spanner]:
1077
        # noinspection PyShadowingNames
1078
        '''
1079
        Given a spanner spannedElement (an object), replace all old spannedElements
1080
        with new spannedElements
1081
        for all Spanner objects contained in this bundle.
1082

1083
        The `old` parameter must be an object, not an object id.
1084

1085
        If no replacements are found, no errors are raised.
1086

1087
        Returns a list of spanners that had elements replaced.
1088

1089
        >>> n1 = note.Note('C')
1090
        >>> n2 = note.Note('D')
1091
        >>> su1 = spanner.Line(n1, n2)
1092
        >>> su2 = spanner.Glissando(n2, n1)
1093
        >>> sb = spanner.SpannerBundle()
1094
        >>> sb.append(su1)
1095
        >>> sb.append(su2)
1096

1097
        >>> su1
1098
        <music21.spanner.Line <music21.note.Note C><music21.note.Note D>>
1099
        >>> su2
1100
        <music21.spanner.Glissando <music21.note.Note D><music21.note.Note C>>
1101

1102
        >>> n3 = note.Note('E')
1103
        >>> replacedSpanners = sb.replaceSpannedElement(n2, n3)
1104
        >>> replacedSpanners == [su1, su2]
1105
        True
1106

1107
        >>> su1
1108
        <music21.spanner.Line <music21.note.Note C><music21.note.Note E>>
1109
        >>> su2
1110
        <music21.spanner.Glissando <music21.note.Note E><music21.note.Note C>>
1111

1112
        * Changed in v7: id() is no longer allowed for `old`.
1113

1114
        >>> sb.replaceSpannedElement(id(n1), n2)
1115
        Traceback (most recent call last):
1116
        TypeError: send elements to replaceSpannedElement(), not ids.
1117
        '''
1118
        if isinstance(old, int):
1✔
1119
            raise TypeError('send elements to replaceSpannedElement(), not ids.')
1✔
1120

1121
        replacedSpanners: list[Spanner] = []
1✔
1122
        # post = self.__class__()  # return a bundle of spanners that had changes
1123
        self._cache.clear()
1✔
1124

1125
        for sp in self._storage:  # Spanners in a list
1✔
1126
            # environLocal.printDebug(['looking at spanner', sp, sp.getSpannedElementIds()])
1127
            sp._cache = {}
1✔
1128
            # accurate, so long as Spanner.__contains__() checks identity, not equality
1129
            # see discussion at https://github.com/cuthbertLab/music21/pull/905
1130
            if old in sp:
1✔
1131
                sp.replaceSpannedElement(old, new)
1✔
1132
                replacedSpanners.append(sp)
1✔
1133
                # post.append(sp)
1134
                # environLocal.printDebug(['replaceSpannedElement()', sp, 'old', old,
1135
                #    'id(old)', id(old), 'new', new, 'id(new)', id(new)])
1136

1137
        self._cache.clear()
1✔
1138

1139
        return replacedSpanners
1✔
1140

1141
    def getByClass(self, searchClass: str|type|tuple[type, ...]) -> 'SpannerBundle':
1✔
1142
        '''
1143
        Given a spanner class, return a new SpannerBundle of all Spanners of the desired class.
1144

1145
        >>> su1 = spanner.Slur()
1146
        >>> su2 = layout.StaffGroup()
1147
        >>> su3 = layout.StaffGroup()
1148
        >>> sb = spanner.SpannerBundle()
1149
        >>> sb.append(su1)
1150
        >>> sb.append(su2)
1151
        >>> sb.append(su3)
1152

1153
        `searchClass` should be a Class.
1154

1155
        >>> slurs = sb.getByClass(spanner.Slur)
1156
        >>> slurs
1157
        <music21.spanner.SpannerBundle of size 1>
1158
        >>> list(slurs) == [su1]
1159
        True
1160
        >>> list(sb.getByClass(spanner.Slur)) == [su1]
1161
        True
1162
        >>> list(sb.getByClass(layout.StaffGroup)) == [su2, su3]
1163
        True
1164

1165
        A tuple of classes can also be given:
1166

1167
        >>> len(sb.getByClass((spanner.Slur, layout.StaffGroup)))
1168
        3
1169

1170
        Note that the ability to search via a string will be removed in
1171
        version 10.
1172
        '''
1173
        # NOTE: this is called very frequently and is optimized.
1174

1175
        cacheKey = f'getByClass-{searchClass}'
1✔
1176
        searchStr = searchClass if isinstance(searchClass, str) else ''
1✔
1177
        searchClasses = () if isinstance(searchClass, str) else searchClass
1✔
1178

1179
        if cacheKey not in self._cache or self._cache[cacheKey] is None:
1✔
1180
            out: list[Spanner] = []
1✔
1181
            for sp in self._storage:
1✔
1182
                if searchStr and searchStr in sp.classes:
1✔
1183
                    out.append(sp)
1✔
1184
                else:
1185
                    if isinstance(sp, searchClasses):
1✔
1186
                        # PyCharm thinks this is a type, not a Spanner
1187
                        # noinspection PyTypeChecker
1188
                        out.append(sp)
1✔
1189
            self._cache[cacheKey] = self.__class__(out)
1✔
1190
        return self._cache[cacheKey]
1✔
1191

1192
    def setIdLocalByClass(self, className, maxId=6):
1✔
1193
        # noinspection PyShadowingNames
1194
        '''
1195
        (See :meth:`setIdLocals` for an explanation of what an idLocal is.)
1196

1197
        Automatically set idLocal values for all members of the provided class.
1198
        This is necessary in cases where spanners are newly created in
1199
        potentially overlapping boundaries and need to be tagged for MusicXML
1200
        or other output. Note that, if some Spanners already have idLocals,
1201
        they will be overwritten.
1202

1203
        The `maxId` parameter sets the largest number that is available for this
1204
        class.  In MusicXML it is 6.
1205

1206
        Currently, this method just iterates over the spanners of this class
1207
        and counts the number from 1-6 and then recycles numbers.  It does
1208
        not check whether more than 6 overlapping spanners of the same type
1209
        exist, nor does it reset the count to 1 after all spanners of that
1210
        class have been closed.  The example below demonstrates that the
1211
        position of the contents of the spanner have no bearing on
1212
        its idLocal (since we don't even put anything into the spanners).
1213

1214

1215
        >>> su1 = spanner.Slur()
1216
        >>> su2 = layout.StaffGroup()
1217
        >>> su3 = spanner.Slur()
1218
        >>> sb = spanner.SpannerBundle()
1219
        >>> sb.append(su1)
1220
        >>> sb.append(su2)
1221
        >>> sb.append(su3)
1222
        >>> [sp.idLocal for sp in sb.getByClass(spanner.Slur)]
1223
        [None, None]
1224
        >>> sb.setIdLocalByClass('Slur')
1225
        >>> [sp.idLocal for sp in sb.getByClass(spanner.Slur)]
1226
        [1, 2]
1227
        '''
1228
        # note that this overrides previous values
1229
        for i, sp in enumerate(self.getByClass(className)):
1✔
1230
            # 6 seems to be limit in musicxml processing
1231
            sp.idLocal = (i % maxId) + 1
1✔
1232

1233
    def setIdLocals(self):
1✔
1234
        # noinspection PyShadowingNames
1235
        '''
1236
        Utility method for outputting MusicXML (and potentially
1237
        other formats) for spanners.
1238

1239
        Each Spanner type (slur, line, glissando, etc.) in MusicXML
1240
        has a number assigned to it.
1241
        We call this number, `idLocal`.  idLocal is a number from 1 to 6.
1242
        This does not mean that your piece can only have six slurs total!
1243
        But it does mean that within a single
1244
        part, only up to 6 slurs can happen simultaneously.
1245
        But as soon as a slur stops, its idLocal can be reused.
1246

1247
        This method sets all idLocals for all classes in this SpannerBundle.
1248
        This will assure that each class has a unique idLocal number.
1249

1250
        Calling this method is destructive: existing idLocal values will be lost.
1251

1252
        >>> su1 = spanner.Slur()
1253
        >>> su2 = layout.StaffGroup()
1254
        >>> su3 = spanner.Slur()
1255
        >>> sb = spanner.SpannerBundle()
1256
        >>> sb.append(su1)
1257
        >>> sb.append(su2)
1258
        >>> sb.append(su3)
1259
        >>> [sp.idLocal for sp in sb.getByClass('Slur')]
1260
        [None, None]
1261
        >>> sb.setIdLocals()
1262
        >>> [(sp, sp.idLocal) for sp in sb]
1263
        [(<music21.spanner.Slur>, 1),
1264
         (<music21.layout.StaffGroup>, 1),
1265
         (<music21.spanner.Slur>, 2)]
1266

1267
        :class:`~music21.dynamics.DynamicWedge` objects are commingled. That is,
1268
        :class:`~music21.dynamics.Crescendo` and
1269
        :class:`~music21.dynamics.Diminuendo`
1270
        are not numbered separately:
1271

1272
        >>> sb2 = spanner.SpannerBundle()
1273
        >>> c = dynamics.Crescendo()
1274
        >>> d = dynamics.Diminuendo()
1275
        >>> sb2.append(c)
1276
        >>> sb2.append(d)
1277
        >>> sb2.setIdLocals()
1278
        >>> [(sp, sp.idLocal) for sp in sb2]
1279
        [(<music21.dynamics.Crescendo>, 1),
1280
         (<music21.dynamics.Diminuendo>, 2)]
1281
        '''
1282
        # Crescendo and Diminuendo share the same numbering
1283
        # So number by DynamicWedge instead (next parent class)
1284
        skip_classes = ('Crescendo', 'Diminuendo')
1✔
1285
        classes = set()
1✔
1286
        for sp in self._storage:
1✔
1287
            for klass in sp.classes:
1✔
1288
                if klass not in skip_classes:
1✔
1289
                    classes.add(klass)
1✔
1290
                    break
1✔
1291
        for className in classes:
1✔
1292
            self.setIdLocalByClass(className)
1✔
1293

1294
    def getByClassIdLocalComplete(self, className, idLocal, completeStatus):
1✔
1295
        '''
1296
        Get all spanners of a specified class `className`, an id `idLocal`, and a `completeStatus`.
1297
        This is a convenience routine for multiple filtering when searching for relevant Spanners
1298
        to pair with.
1299

1300
        >>> su1 = spanner.Slur()
1301
        >>> su2 = layout.StaffGroup()
1302
        >>> su2.idLocal = 3
1303
        >>> sb = spanner.SpannerBundle()
1304
        >>> sb.append(su1)
1305
        >>> sb.append(su2)
1306
        >>> list(sb.getByClassIdLocalComplete(layout.StaffGroup, 3, False)) == [su2]
1307
        True
1308
        >>> su2.completeStatus = True
1309
        >>> list(sb.getByClassIdLocalComplete(layout.StaffGroup, 3, False)) == []
1310
        True
1311
        '''
1312
        # TODO: write utility classes that just modify lists and cast to a spannerBundle
1313
        #    at the end.
1314
        return self.getByClass(className).getByIdLocal(
1✔
1315
            idLocal).getByCompleteStatus(completeStatus)
1316

1317
    def setPendingSpannedElementAssignment(
1✔
1318
        self,
1319
        sp: Spanner,
1320
        className: str,
1321
        offsetInScore: OffsetQL|None = None,
1322
        clientInfo: t.Any|None = None
1323
    ):
1324
        '''
1325
        A SpannerBundle can be set up so that a particular spanner (sp) is looking
1326
        for an element of class (className) to be set as first element. Any future
1327
        future element that matches the className (and offsetInScore, if specified)
1328
        which is passed to the SpannerBundle via freePendingFirstSpannedElementAssignment()
1329
        will get it.  clientInfo is not used in the match, but can be used by the client
1330
        when cleaning up any leftover pending assignments, by creating SpannerAnchors
1331
        at the appropriate offset.
1332

1333
        >>> n1 = note.Note('C')
1334
        >>> r1 = note.Rest()
1335
        >>> n2 = note.Note('D')
1336
        >>> n2Wrong = note.Note('B')
1337
        >>> n3 = note.Note('E')
1338
        >>> su1 = spanner.Slur([n1])
1339
        >>> sb = spanner.SpannerBundle()
1340
        >>> sb.append(su1)
1341
        >>> su1.getSpannedElements()
1342
        [<music21.note.Note C>]
1343

1344
        >>> n1.getSpannerSites()
1345
        [<music21.spanner.Slur <music21.note.Note C>>]
1346

1347
        Now set up su1 to get the next note assigned to it.
1348

1349
        >>> sb.setPendingSpannedElementAssignment(su1, 'Note', 0.)
1350

1351
        Call freePendingSpannedElementAssignment to attach.
1352

1353
        Should not get a note at the wrong offset.
1354

1355
        >>> sb.freePendingSpannedElementAssignment(n2Wrong, 1.)
1356
        >>> su1.getSpannedElements()
1357
        [<music21.note.Note C>]
1358

1359
        Should not get a rest, because it is not a 'Note'
1360

1361
        >>> sb.freePendingSpannedElementAssignment(r1, 0.)
1362
        >>> su1.getSpannedElements()
1363
        [<music21.note.Note C>]
1364

1365
        But will get the next note:
1366

1367
        >>> sb.freePendingSpannedElementAssignment(n2, 0.)
1368
        >>> su1.getSpannedElements()
1369
        [<music21.note.Note D>, <music21.note.Note C>]
1370

1371
        >>> n2.getSpannerSites()
1372
        [<music21.spanner.Slur <music21.note.Note D><music21.note.Note C>>]
1373

1374
        And now that the assignment has been made, the pending assignment
1375
        has been cleared, so n3 will not get assigned to the slur:
1376

1377
        >>> sb.freePendingSpannedElementAssignment(n3, 0.)
1378
        >>> su1.getSpannedElements()
1379
        [<music21.note.Note D>, <music21.note.Note C>]
1380

1381
        >>> n3.getSpannerSites()
1382
        []
1383

1384
        '''
1385
        ref: PendingAssignmentRef = {
1✔
1386
            'spanner': sp,
1387
            'className': className,
1388
            'offsetInScore': offsetInScore,
1389
            'clientInfo': clientInfo
1390
        }
1391
        self._pendingSpannedElementAssignment.append(ref)
1✔
1392

1393
    def freePendingSpannedElementAssignment(
1✔
1394
        self,
1395
        spannedElementCandidate,
1396
        offsetInScore: OffsetQL|None = None
1397
    ):
1398
        '''
1399
        Assigns and frees up a pendingSpannedElementAssignment if one
1400
        is active and the candidate matches the class (and offsetInScore,
1401
        if specified).  See  setPendingSpannedElementAssignment for
1402
        documentation and tests.
1403

1404
        It is set up via a first-in, first-out priority.
1405
        '''
1406

1407
        if not self._pendingSpannedElementAssignment:
1✔
1408
            return
1✔
1409

1410
        remove = None
1✔
1411
        for i, ref in enumerate(self._pendingSpannedElementAssignment):
1✔
1412
            # environLocal.printDebug(['calling freePendingSpannedElementAssignment()',
1413
            #    self._pendingSpannedElementAssignment])
1414
            if ref['className'] in spannedElementCandidate.classSet:
1✔
1415
                if (offsetInScore is None
1✔
1416
                        or offsetInScore == ref['offsetInScore']):
1417
                    ref['spanner'].insertFirstSpannedElement(spannedElementCandidate)
1✔
1418
                    remove = i
1✔
1419
                    # environLocal.printDebug(['freePendingSpannedElementAssignment()',
1420
                    #    'added spannedElement', ref['spanner']])
1421
                    break
1✔
1422
        if remove is not None:
1✔
1423
            self._pendingSpannedElementAssignment.pop(remove)
1✔
1424

1425
    def popPendingSpannedElementAssignments(self) -> list[PendingAssignmentRef]:
1✔
1426
        '''
1427
        Removes and returns all pendingSpannedElementAssignments.
1428
        This can be called when there will be no more calls to
1429
        freePendingSpannedElementAssignment, and SpannerAnchors
1430
        need to be created for each remaining pending assignment.
1431
        The SpannerAnchors should be created at the appropriate
1432
        offset, dictated by the assignment's offsetInScore.
1433
        '''
1434
        output: list[PendingAssignmentRef] = self._pendingSpannedElementAssignment
1✔
1435
        self._pendingSpannedElementAssignment = []
1✔
1436
        return output
1✔
1437

1438
# ------------------------------------------------------------------------------
1439
# connect two or more notes anywhere in the score
1440
class Slur(Spanner):
1✔
1441
    '''
1442
    A slur represented as a spanner between two Notes.
1443

1444
    Slurs have `.placement` options ('above' or 'below') and `.lineType` ('dashed' or None)
1445
    '''
1446

1447
    def __init__(self, *spannedElements, **keywords):
1✔
1448
        super().__init__(*spannedElements, **keywords)
1✔
1449
        self.placement = None  # can above or below, after musicxml
1✔
1450
        self.lineType = None  # can be 'dashed' or None
1✔
1451
        # from music21 import note
1452
        # self.fillElementTypes = [note.NotRest]
1453

1454
    # TODO: add property for placement
1455

1456
# ------------------------------------------------------------------------------
1457

1458

1459
class MultiMeasureRest(Spanner):
1✔
1460
    '''
1461
    A grouping symbol that indicates that a collection of rests lasts
1462
    multiple measures.
1463
    '''
1464
    _styleClass = style.TextStyle
1✔
1465

1466
    _DOC_ATTR: dict[str, str] = {
1✔
1467
        'useSymbols': '''
1468
            Boolean to indicate whether rest symbols
1469
            (breve, longa, etc.) should be used when
1470
            displaying the rest. Your music21 inventor
1471
            is a medievalist, so this defaults to True.
1472

1473
            Change defaults.multiMeasureRestUseSymbols to
1474
            change globally.
1475
            ''',
1476
        'maxSymbols': '''
1477
            An int, specifying the maximum number of rests
1478
            to display as symbols.  Default is 11.
1479
            If useSymbols is False then this setting
1480
            does nothing.
1481

1482
            Change defaults.multiMeasureRestMaxSymbols to
1483
            change globally.
1484
            ''',
1485
    }
1486

1487
    def __init__(self, *spannedElements, **keywords):
1✔
1488
        super().__init__(*spannedElements, **keywords)
1✔
1489

1490
        # from music21 import note
1491
        # self.fillElementTypes = [note.Rest]
1492
        self._overriddenNumber = None
1✔
1493
        self.useSymbols = keywords.get('useSymbols', defaults.multiMeasureRestUseSymbols)
1✔
1494
        self.maxSymbols = keywords.get('maxSymbols', defaults.multiMeasureRestMaxSymbols)
1✔
1495

1496
    def _reprInternal(self):
1✔
1497
        plural = 's' if self.numRests != 1 else ''
1✔
1498
        return f'{self.numRests} measure{plural}'
1✔
1499

1500
    @property
1✔
1501
    def numRests(self):
1✔
1502
        '''
1503
        Returns the number of measures involved in the
1504
        multi-measure rest.
1505

1506
        Calculated automatically from the number of rests in
1507
        the spanner.  Or can be set manually to override the number.
1508

1509
        >>> mmr = spanner.MultiMeasureRest()
1510
        >>> for i in range(6):
1511
        ...     mmr.addSpannedElements([note.Rest(type='whole')])
1512
        >>> mmr.numRests
1513
        6
1514
        >>> mmr.numRests = 10
1515
        >>> mmr.numRests
1516
        10
1517
        '''
1518
        if self._overriddenNumber is not None:
1✔
1519
            return self._overriddenNumber
1✔
1520
        else:
1521
            return len(self)
1✔
1522

1523
    @numRests.setter
1✔
1524
    def numRests(self, overridden):
1✔
1525
        self._overriddenNumber = overridden
1✔
1526

1527
# ------------------------------------------------------------------------------
1528
# first/second repeat bracket
1529

1530

1531
class RepeatBracket(Spanner):
1✔
1532
    '''
1533
    A grouping of one or more measures, presumably in sequence, that mark an alternate repeat.
1534

1535
    These gather what are sometimes called first-time bars and second-time bars.
1536

1537
    It is assumed that numbering starts from 1. Numberings above 2 are permitted.
1538
    The `number` keyword argument can be used to pass in the desired number.
1539

1540
    `overrideDisplay` if set will display something other than the number.  For instance
1541
    `ouvert` and `clos` for medieval music.  However, if you use it for something like '1-3'
1542
    be sure to set number properly too.
1543

1544
    >>> m = stream.Measure()
1545
    >>> sp = spanner.RepeatBracket(m, number=1)
1546
    >>> sp  # can be one or more measures
1547
    <music21.spanner.RepeatBracket 1 <music21.stream.Measure 0 offset=0.0>>
1548

1549
    >>> sp.number = 3
1550
    >>> sp
1551
    <music21.spanner.RepeatBracket 3 <music21.stream.Measure 0 offset=0.0>>
1552
    >>> sp.numberRange  # the list of repeat numbers
1553
    [3]
1554
    >>> sp.number
1555
    '3'
1556

1557
    Range of repeats as string:
1558

1559
    >>> sp.number = '1-3'
1560
    >>> sp.numberRange
1561
    [1, 2, 3]
1562
    >>> sp.number
1563
    '1-3'
1564

1565
    Range of repeats as list:
1566

1567
    >>> sp.number = [2, 3]
1568
    >>> sp.numberRange
1569
    [2, 3]
1570
    >>> sp.number
1571
    '2, 3'
1572

1573
    Comma separated numbers:
1574

1575
    >>> sp.number = '1, 2, 3'
1576
    >>> sp.numberRange
1577
    [1, 2, 3]
1578
    >>> sp.number
1579
    '1-3'
1580

1581
    Disjunct numbers:
1582

1583
    >>> sp.number = '1, 2, 3, 7'
1584
    >>> sp.numberRange
1585
    [1, 2, 3, 7]
1586
    >>> sp.number
1587
    '1, 2, 3, 7'
1588

1589
    Override the display.
1590

1591
    >>> sp.overrideDisplay = '1-3, 7'
1592
    >>> sp
1593
    <music21.spanner.RepeatBracket 1-3, 7
1594
         <music21.stream.Measure 0 offset=0.0>>
1595

1596
    number is not affected by display overrides:
1597

1598
    >>> sp.number
1599
    '1, 2, 3, 7'
1600
    '''
1601

1602
    _DOC_ATTR = {
1✔
1603
        'numberRange': '''
1604
            Get a contiguous list of repeat numbers that are applicable for this instance.
1605

1606
            Will always have at least one element, but [0] means undefined
1607

1608
            >>> rb = spanner.RepeatBracket()
1609
            >>> rb.numberRange
1610
            [0]
1611

1612
            >>> rb.number = '1,2'
1613
            >>> rb.numberRange
1614
            [1, 2]
1615
        ''',
1616
        'overrideDisplay': '''
1617
            Override the string representation of this bracket, or use
1618
            None to not override.
1619
        ''',
1620
    }
1621

1622
    def __init__(self,
1✔
1623
                 *spannedElements,
1624
                 number: int|str|Iterable[int] = 0,
1625
                 overrideDisplay: str|None = None,
1626
                 **keywords):
1627
        super().__init__(*spannedElements, **keywords)
1✔
1628

1629
        # from music21 import stream
1630
        # self.fillElementTypes = [stream.Measure]
1631

1632
        # store a range, inclusive of the single number assignment
1633
        self.numberRange: list[int] = []
1✔
1634
        self.overrideDisplay = overrideDisplay
1✔
1635
        self.number = number
1✔
1636

1637
    @property
1✔
1638
    def _numberSpanIsAdjacent(self) -> bool:
1✔
1639
        '''
1640
        are there exactly two numbers that should be written as 3, 4 not 3-4.
1641
        '''
1642
        return len(self.numberRange) == 2 and self.numberRange[0] == self.numberRange[1] - 1
1✔
1643

1644
    @property
1✔
1645
    def _numberSpanIsContiguous(self) -> bool:
1✔
1646
        '''
1647
        can we write as '3, 4' or '5-10' and not as '1, 5, 6, 11'
1648
        '''
1649
        return common.contiguousList(self.numberRange)
1✔
1650

1651
    # property to enforce numerical numbers
1652
    def _getNumber(self) -> str:
1✔
1653
        '''
1654
        This must return a string, as we may have single numbers or lists.
1655
        For a raw numerical list, look at `.numberRange`.
1656
        '''
1657
        if len(self.numberRange) == 1:
1✔
1658
            if self.numberRange[0] == 0:
1✔
1659
                return ''
1✔
1660
            return str(self.numberRange[0])
1✔
1661
        else:
1662
            if not self._numberSpanIsContiguous:
1✔
1663
                return ', '.join([str(x) for x in self.numberRange])
1✔
1664
            elif self._numberSpanIsAdjacent:
1✔
1665
                return f'{self.numberRange[0]}, {self.numberRange[-1]}'
1✔
1666
            else:  # range of values
1667
                return f'{self.numberRange[0]}-{self.numberRange[-1]}'
1✔
1668

1669
    def _setNumber(self, value: int|str|Iterable[int]):
1✔
1670
        '''
1671
        Set the bracket number. There may be range of values provided
1672
        '''
1673
        if value == '':
1✔
1674
            # undefined.
1675
            self.numberRange = [0]
×
1676
        elif common.holdsType(value, int):
1✔
1677
            self.numberRange = []  # clear
1✔
1678
            for x in value:
1✔
1679
                self.numberRange.append(x)
1✔
1680
        elif isinstance(value, str):
1✔
1681
            # assume defined a range with a dash; assumed inclusive
1682
            if '-' in value:
1✔
1683
                start, end = value.split('-')
1✔
1684
                self.numberRange = list(range(int(start), int(end) + 1))
1✔
1685

1686
            elif ',' in value:
1✔
1687
                self.numberRange = []  # clear
1✔
1688
                for one_letter_value in value.split(','):
1✔
1689
                    one_number = int(one_letter_value.strip())
1✔
1690
                    self.numberRange.append(one_number)
1✔
1691
            elif value.isdigit():
1✔
1692
                self.numberRange = [int(value)]
1✔
1693
            else:
1694
                raise SpannerException(f'number for RepeatBracket must be a number, not {value!r}')
1✔
1695
        elif common.isInt(value):
1✔
1696
            self.numberRange = []  # clear
1✔
1697
            if value not in self.numberRange:
1✔
1698
                self.numberRange.append(value)
1✔
1699
        else:
1700
            raise SpannerException(f'number for RepeatBracket must be a number, not {value!r}')
×
1701

1702
    number = property(_getNumber, _setNumber, doc='''
1✔
1703
            Get or set the number -- returning a string always.
1704

1705
            >>> rb = spanner.RepeatBracket()
1706
            >>> rb.number
1707
            ''
1708
            >>> rb.number = '5-7'
1709
            >>> rb.number
1710
            '5-7'
1711
            >>> rb.numberRange
1712
            [5, 6, 7]
1713
            >>> rb.number = 1
1714
        ''')
1715

1716
    @common.deprecated('v9', 'v10', 'Look at .numberRange instead')
1717
    def getNumberList(self):  # pragma: no cover
1718
        '''
1719
        Deprecated -- just look at .numberRange
1720
        '''
1721
        return self.numberRange
1722

1723
    def _reprInternal(self):
1✔
1724
        if self.overrideDisplay is not None:
1✔
1725
            msg = self.overrideDisplay + ' '
1✔
1726
        else:
1727
            msg = self.number + ' '
1✔
1728
        return msg + super()._reprInternal()
1✔
1729

1730

1731
# ------------------------------------------------------------------------------
1732
# line-based spanners
1733

1734
class Ottava(Spanner):
1✔
1735
    '''
1736
    An octave shift line:
1737

1738
    >>> ottava = spanner.Ottava(type='8va')
1739
    >>> ottava.type
1740
    '8va'
1741
    >>> ottava.type = 15
1742
    >>> ottava.type
1743
    '15ma'
1744
    >>> ottava.type = (8, 'down')
1745
    >>> ottava.type
1746
    '8vb'
1747
    >>> print(ottava)
1748
    <music21.spanner.Ottava 8vb transposing>
1749

1750
    An Ottava spanner can either be transposing or non-transposing.
1751
    In a transposing Ottava spanner, the notes in the stream should be
1752
    in their written octave (as if the spanner were not there) and all the
1753
    notes in the spanner will be transposed on Stream.toSoundingPitch().
1754

1755
    A non-transposing spanner has notes that are at the pitch that
1756
    they would sound (therefore the Ottava spanner is a decorative
1757
    line).
1758

1759
    >>> ottava.transposing
1760
    True
1761
    >>> n1 = note.Note('D4')
1762
    >>> n2 = note.Note('E4')
1763
    >>> n2.offset = 2.0
1764
    >>> ottava.addSpannedElements([n1, n2])
1765

1766
    >>> s = stream.Stream([ottava, n1, n2])
1767
    >>> s.atSoundingPitch = False
1768
    >>> s2 = s.toSoundingPitch()
1769
    >>> s2.show('text')
1770
    {0.0} <music21.spanner.Ottava 8vb non-transposing<music21.note.Note D><music21.note.Note E>>
1771
    {0.0} <music21.note.Note D>
1772
    {2.0} <music21.note.Note E>
1773

1774
    >>> for n in s2.notes:
1775
    ...     print(n.nameWithOctave)
1776
    D3
1777
    E3
1778

1779
    All valid types are given below:
1780

1781
    >>> ottava.validOttavaTypes
1782
    ('8va', '8vb', '15ma', '15mb', '22da', '22db')
1783

1784
    OMIT_FROM_DOCS
1785

1786
    Test the round-trip back:
1787

1788
    >>> s3 = s2.toWrittenPitch()
1789
    >>> s3.show('text')
1790
    {0.0} <music21.spanner.Ottava 8vb transposing<music21.note.Note D><music21.note.Note E>>
1791
    {0.0} <music21.note.Note D>
1792
    {2.0} <music21.note.Note E>
1793

1794
    >>> for n in s3.notes:
1795
    ...    print(n.nameWithOctave)
1796
    D4
1797
    E4
1798
    '''
1799
    validOttavaTypes = ('8va', '8vb', '15ma', '15mb', '22da', '22db')
1✔
1800

1801
    def __init__(self,
1✔
1802
                 *spannedElements,
1803
                 type: str = '8va',  # pylint: disable=redefined-builtin
1804
                 transposing: bool = True,
1805
                 placement: t.Literal['above', 'below'] = 'above',
1806
                 **keywords):
1807
        from music21 import note
1✔
1808
        super().__init__(*spannedElements, **keywords)
1✔
1809
        self.fillElementTypes = [note.NotRest]
1✔
1810
        self._type = None  # can be 8va, 8vb, 15ma, 15mb
1✔
1811
        self.type = type
1✔
1812

1813
        self.placement = placement  # can above or below, after musicxml
1✔
1814
        self.transposing = transposing
1✔
1815

1816
    def _getType(self):
1✔
1817
        return self._type
1✔
1818

1819
    def _setType(self, newType):
1✔
1820
        if common.isNum(newType) and newType in (8, 15):
1✔
1821
            if newType == 8:
1✔
1822
                self._type = '8va'
×
1823
            else:
1824
                self._type = '15ma'
1✔
1825
        # try to parse as list of size, dir
1826
        elif common.isListLike(newType) and len(newType) >= 1:
1✔
1827
            stub = []
1✔
1828
            if newType[0] in (8, '8'):
1✔
1829
                stub.append(str(newType[0]))
1✔
1830
                stub.append('v')
1✔
1831
            elif newType[0] in (15, '15'):
1✔
1832
                stub.append(str(newType[0]))
1✔
1833
                stub.append('m')
1✔
1834
            if len(newType) >= 2 and newType[1] == 'down':
1✔
1835
                stub.append('b')
1✔
1836
            else:  # default if not provided
1837
                stub.append('a')
1✔
1838
            self._type = ''.join(stub)
1✔
1839
        else:
1840
            if (not isinstance(newType, str)
1✔
1841
                    or newType.lower() not in self.validOttavaTypes):
1842
                raise SpannerException(
×
1843
                    f'cannot create Ottava of type: {newType}')
1844
            self._type = newType.lower()
1✔
1845

1846
    type = property(_getType, _setType, doc='''
1✔
1847
        Get or set Ottava type. This can be set by as complete string
1848
        (such as 8va or 15mb) or with a pair specifying size and direction.
1849

1850
        >>> os = spanner.Ottava()
1851
        >>> os.type = '8vb'
1852
        >>> os.type
1853
        '8vb'
1854
        >>> os.type = 15, 'down'
1855
        >>> os.type
1856
        '15mb'
1857
        ''')
1858

1859
    def _reprInternal(self):
1✔
1860
        transposing = 'transposing'
1✔
1861
        if not self.transposing:
1✔
1862
            transposing = 'non-transposing'
1✔
1863
        return f'{self.type} {transposing}' + super()._reprInternal()
1✔
1864

1865
    def shiftMagnitude(self):
1✔
1866
        '''
1867
        Get basic parameters of shift.
1868

1869
        Returns either 8, 15, or 22 depending on the amount of shift
1870
        '''
1871
        if self._type.startswith('8'):
1✔
1872
            return 8
1✔
1873
        elif self._type.startswith('15'):
1✔
1874
            return 15
1✔
1875
        elif self._type.startswith('22'):
×
1876
            return 22
×
1877
        else:
1878
            raise SpannerException(f'Cannot get shift magnitude from {self._type!r}')
×
1879

1880
    def shiftDirection(self, reverse=False):
1✔
1881
        '''
1882
        Returns up or down depending on the type of shift:
1883
        '''
1884
        # an 8va mark means that the notes must be shifted down with the mark
1885
        if self._type.endswith('a'):
1✔
1886
            if reverse:
1✔
1887
                return 'down'
1✔
1888
            else:
1889
                return 'up'
1✔
1890
        # an 8vb means that the notes must be shifted upward with the mark
1891
        if self._type.endswith('b'):
1✔
1892
            if reverse:
1✔
1893
                return 'up'
1✔
1894
            else:
1895
                return 'down'
1✔
1896

1897
    def interval(self, reverse=False):
1✔
1898
        '''
1899
        return an interval.Interval() object representing this ottava
1900

1901
        >>> ottava = spanner.Ottava(type='15mb')
1902
        >>> i = ottava.interval()
1903
        >>> i
1904
        <music21.interval.Interval P-15>
1905
        '''
1906
        from music21.interval import Interval
1✔
1907
        if self.shiftDirection(reverse=reverse) == 'down':
1✔
1908
            header = 'P-'
1✔
1909
        else:
1910
            header = 'P'
1✔
1911

1912
        header += str(self.shiftMagnitude())
1✔
1913
        return Interval(header)
1✔
1914

1915
    def performTransposition(self):
1✔
1916
        '''
1917
        On a transposing spanner, switch to non-transposing,
1918
        and transpose all notes and chords in the spanner.
1919
        The note/chords will all be transposed to their sounding
1920
        pitch (at least as far as the ottava is concerned;
1921
        transposing instruments are handled separately).
1922

1923
        >>> ottava = spanner.Ottava(type='8va')
1924
        >>> n1 = note.Note('D#4')
1925
        >>> n2 = note.Note('E#4')
1926
        >>> ottava.addSpannedElements([n1, n2])
1927
        >>> ottava.transposing
1928
        True
1929

1930
        >>> ottava.performTransposition()
1931

1932
        >>> ottava.transposing
1933
        False
1934
        >>> n1.nameWithOctave
1935
        'D#5'
1936
        '''
1937
        if not self.transposing:
1✔
1938
            return
1✔
1939
        self.transposing = False
1✔
1940

1941
        myInterval = self.interval()
1✔
1942
        for n in self.getSpannedElements():
1✔
1943
            if not hasattr(n, 'pitches'):
1✔
1944
                continue
×
1945
            for p in n.pitches:
1✔
1946
                p.transpose(myInterval, inPlace=True)
1✔
1947

1948
    def undoTransposition(self):
1✔
1949
        '''
1950
        Change a non-transposing spanner to a transposing spanner,
1951
        and transpose back all the notes and chords in the spanner.
1952
        The notes/chords will all be transposed to their written
1953
        pitch (at least as far as the ottava is concerned;
1954
        transposing instruments are handled separately).
1955

1956
        >>> ottava = spanner.Ottava(type='8va')
1957
        >>> n1 = note.Note('D#4')
1958
        >>> n2 = note.Note('E#4')
1959
        >>> ottava.addSpannedElements([n1, n2])
1960
        >>> ottava.transposing = False
1961

1962
        >>> ottava.undoTransposition()
1963

1964
        >>> ottava.transposing
1965
        True
1966
        >>> n1.nameWithOctave
1967
        'D#3'
1968
        '''
1969
        if self.transposing:
1✔
1970
            return
1✔
1971
        self.transposing = True
1✔
1972

1973
        myInterval = self.interval(reverse=True)
1✔
1974
        for n in self.getSpannedElements():
1✔
1975
            if not hasattr(n, 'pitches'):
1✔
1976
                continue
×
1977
            for p in n.pitches:
1✔
1978
                p.transpose(myInterval, inPlace=True)
1✔
1979

1980

1981

1982
class Line(Spanner):
1✔
1983
    '''
1984
    A line or bracket represented as a spanner above two Notes.
1985

1986
    Brackets can take many line types.
1987

1988
    >>> b = spanner.Line()
1989
    >>> b.lineType = 'dotted'
1990
    >>> b.lineType
1991
    'dotted'
1992
    >>> b = spanner.Line(endHeight=20)
1993
    >>> b.endHeight
1994
    20
1995
    '''
1996
    validLineTypes = ('solid', 'dashed', 'dotted', 'wavy')
1✔
1997
    validTickTypes = ('up', 'down', 'arrow', 'both', 'none')
1✔
1998

1999
    def __init__(
1✔
2000
        self,
2001
        *spannedElements,
2002
        lineType: str = 'solid',
2003
        tick: str = 'down',
2004
        startTick: str = 'down',
2005
        endTick: str = 'down',
2006
        startHeight: int|float|None = None,
2007
        endHeight: int|float|None = None,
2008
        **keywords
2009
    ):
2010
        super().__init__(*spannedElements, **keywords)
1✔
2011
        # from music21 import note
2012
        # self.fillElementTypes = [note.GeneralNote]
2013

2014
        DEFAULT_TICK = 'down'
1✔
2015
        self._endTick = DEFAULT_TICK  # can be up/down/arrow/both/None
1✔
2016
        self._startTick = DEFAULT_TICK  # can be up/down/arrow/both/None
1✔
2017

2018
        self._endHeight = None  # for up/down, specified in tenths
1✔
2019
        self._startHeight = None  # for up/down, specified in tenths
1✔
2020

2021
        DEFAULT_LINE_TYPE = 'solid'
1✔
2022
        self._lineType = DEFAULT_LINE_TYPE  # can be solid, dashed, dotted, wavy
1✔
2023

2024
        DEFAULT_PLACEMENT = 'above'
1✔
2025
        self.placement = DEFAULT_PLACEMENT  # can above or below, after musicxml
1✔
2026

2027
        if lineType != DEFAULT_LINE_TYPE:
1✔
2028
            self.lineType = lineType  # use property
1✔
2029

2030
        if startTick != DEFAULT_TICK:
1✔
2031
            self.startTick = startTick  # use property
1✔
2032
        if endTick != DEFAULT_TICK:
1✔
2033
            self.endTick = endTick  # use property
1✔
2034
        if tick != DEFAULT_TICK:
1✔
2035
            self.tick = tick  # use property
×
2036

2037
        if endHeight is not None:
1✔
2038
            self.endHeight = endHeight  # use property
1✔
2039
        if startHeight is not None:
1✔
2040
            self.startHeight = startHeight  # use property
×
2041

2042
    def _getEndTick(self):
1✔
2043
        return self._endTick
1✔
2044

2045
    def _setEndTick(self, value):
1✔
2046
        if value.lower() not in self.validTickTypes:
1✔
2047
            raise SpannerException(f'not a valid value: {value}')
×
2048
        self._endTick = value.lower()
1✔
2049

2050
    endTick = property(_getEndTick, _setEndTick, doc='''
1✔
2051
        Get or set the endTick property.
2052
        ''')
2053

2054
    def _getStartTick(self):
1✔
2055
        return self._startTick
1✔
2056

2057
    def _setStartTick(self, value):
1✔
2058
        if value.lower() not in self.validTickTypes:
1✔
2059
            raise SpannerException(f'not a valid value: {value}')
×
2060
        self._startTick = value.lower()
1✔
2061

2062
    startTick = property(_getStartTick, _setStartTick, doc='''
1✔
2063
        Get or set the startTick property.
2064
        ''')
2065

2066
    def _getTick(self):
1✔
2067
        return self._startTick  # just returning start
×
2068

2069
    def _setTick(self, value):
1✔
2070
        if value.lower() not in self.validTickTypes:
1✔
2071
            raise SpannerException(f'not a valid value: {value}')
×
2072
        self._startTick = value.lower()
1✔
2073
        self._endTick = value.lower()
1✔
2074

2075
    tick = property(_getTick, _setTick, doc='''
1✔
2076
        Set the start and end tick to the same value
2077

2078

2079
        >>> b = spanner.Line()
2080
        >>> b.tick = 'arrow'
2081
        >>> b.startTick
2082
        'arrow'
2083
        >>> b.endTick
2084
        'arrow'
2085
        ''')
2086

2087
    def _getLineType(self):
1✔
2088
        return self._lineType
1✔
2089

2090
    def _setLineType(self, value):
1✔
2091
        if value is not None and value.lower() not in self.validLineTypes:
1✔
2092
            raise SpannerException(f'not a valid value: {value}')
1✔
2093
        # not sure if we should permit setting as None
2094
        if value is not None:
1✔
2095
            self._lineType = value.lower()
1✔
2096

2097
    lineType = property(_getLineType, _setLineType, doc='''
1✔
2098
        Get or set the lineType property. Valid line types are listed in .validLineTypes.
2099

2100
        >>> b = spanner.Line()
2101
        >>> b.lineType = 'dotted'
2102
        >>> b.lineType = 'navyblue'
2103
        Traceback (most recent call last):
2104
        music21.spanner.SpannerException: not a valid value: navyblue
2105

2106
        >>> b.validLineTypes
2107
        ('solid', 'dashed', 'dotted', 'wavy')
2108
        ''')
2109

2110
    def _getEndHeight(self):
1✔
2111
        return self._endHeight
1✔
2112

2113
    def _setEndHeight(self, value):
1✔
2114
        if not (common.isNum(value) and value >= 0):
1✔
2115
            raise SpannerException(f'not a valid value: {value}')
1✔
2116
        self._endHeight = value
1✔
2117

2118
    endHeight = property(_getEndHeight, _setEndHeight, doc='''
1✔
2119
        Get or set the endHeight property.
2120

2121
        >>> b = spanner.Line()
2122
        >>> b.endHeight = -20
2123
        Traceback (most recent call last):
2124
        music21.spanner.SpannerException: not a valid value: -20
2125
        ''')
2126

2127
    def _getStartHeight(self):
1✔
2128
        return self._startHeight
1✔
2129

2130
    def _setStartHeight(self, value):
1✔
2131
        if not (common.isNum(value) and value >= 0):
1✔
2132
            raise SpannerException(f'not a valid value: {value}')
1✔
2133
        self._startHeight = value
1✔
2134

2135
    startHeight = property(_getStartHeight, _setStartHeight, doc='''
1✔
2136
        Get or set the startHeight property.
2137

2138
        >>> b = spanner.Line()
2139
        >>> b.startHeight = None
2140
        Traceback (most recent call last):
2141
        music21.spanner.SpannerException: not a valid value: None
2142
        ''')
2143

2144

2145
class Glissando(Spanner):
1✔
2146
    '''
2147
    A between two Notes specifying a glissando or similar alteration.
2148
    Different line types can be specified.
2149

2150
    Glissandos can have a label and a lineType.  Label is a string or None.
2151
    lineType defaults to 'wavy'
2152

2153
    >>> gl = spanner.Glissando()
2154
    >>> gl.lineType
2155
    'wavy'
2156
    >>> print(gl.label)
2157
    None
2158

2159
    >>> gl.label = 'gliss.'
2160

2161
    Note -- not a Line subclass for now, but that might change.
2162
    '''
2163
    validLineTypes = ('solid', 'dashed', 'dotted', 'wavy')
1✔
2164
    validSlideTypes = ('chromatic', 'continuous', 'diatonic', 'white', 'black')
1✔
2165

2166
    def __init__(self,
1✔
2167
                 *spannedElements,
2168
                 lineType: str = 'wavy',
2169
                 label: str|None = None,
2170
                 **keywords):
2171
        super().__init__(*spannedElements, **keywords)
1✔
2172
        # from music21 import note
2173
        # self.fillElementTypes = [note.NotRest]
2174

2175
        GLISSANDO_DEFAULT_LINE_TYPE = 'wavy'
1✔
2176
        self._lineType = GLISSANDO_DEFAULT_LINE_TYPE
1✔
2177
        self._slideType = 'chromatic'
1✔
2178

2179
        self.label = None
1✔
2180

2181
        if lineType != GLISSANDO_DEFAULT_LINE_TYPE:
1✔
2182
            self.lineType = lineType  # use property
×
2183
        if label is not None:
1✔
2184
            self.label = label  # use property
×
2185

2186
    def _getLineType(self):
1✔
2187
        return self._lineType
1✔
2188

2189
    def _setLineType(self, value):
1✔
2190
        if value.lower() not in self.validLineTypes:
1✔
2191
            raise SpannerException(f'not a valid value: {value}')
×
2192
        self._lineType = value.lower()
1✔
2193

2194
    lineType = property(_getLineType, _setLineType, doc='''
1✔
2195
        Get or set the lineType property. See Line for valid line types.
2196
        ''')
2197

2198
    @property
1✔
2199
    def slideType(self):
1✔
2200
        '''
2201
        Get or set the slideType which determines how
2202
        the glissando or slide is to be played.  Values
2203
        are 'chromatic' (default), 'continuous' (like a slide or smear),
2204
        'diatonic' (like a harp gliss), 'white' (meaning a white-key gliss
2205
        as on a marimba), or 'black' (black-key gliss).
2206

2207
        'continuous' slides export to MusicXML as a <slide> object.
2208
        All others export as <glissando>.
2209
        '''
2210
        return self._slideType
1✔
2211

2212
    @slideType.setter
1✔
2213
    def slideType(self, value):
1✔
2214
        if value.lower() not in self.validSlideTypes:
1✔
2215
            raise SpannerException(f'not a valid value: {value}')
×
2216
        self._slideType = value.lower()
1✔
2217

2218
# ------------------------------------------------------------------------------
2219

2220
# pylint: disable=redefined-outer-name
2221
class Test(unittest.TestCase):
1✔
2222

2223
    def setUp(self):
1✔
2224
        from music21.musicxml.m21ToXml import GeneralObjectExporter
1✔
2225
        self.GEX = GeneralObjectExporter()
1✔
2226

2227
    def xmlStr(self, obj):
1✔
2228
        xmlBytes = self.GEX.parse(obj)
1✔
2229
        return xmlBytes.decode('utf-8')
1✔
2230

2231
    def testCopyAndDeepcopy(self):
1✔
2232
        from music21.test.commonTest import testCopyAll
1✔
2233
        testCopyAll(self, globals())
1✔
2234

2235
    def testBasic(self):
1✔
2236

2237
        # how parts might be grouped
2238
        from music21 import stream
1✔
2239
        from music21 import note
1✔
2240
        from music21 import layout
1✔
2241
        s = stream.Score()
1✔
2242
        p1 = stream.Part()
1✔
2243
        p2 = stream.Part()
1✔
2244

2245
        sg1 = layout.StaffGroup(p1, p2)
1✔
2246

2247
        # place all on Stream
2248
        s.insert(0, p1)
1✔
2249
        s.insert(0, p2)
1✔
2250
        s.insert(0, sg1)
1✔
2251

2252
        self.assertEqual(len(s), 3)
1✔
2253
        self.assertEqual(sg1.getSpannedElements(), [p1, p2])
1✔
2254

2255
        # make sure spanners is unified
2256

2257
        # how slurs might be defined
2258
        n1 = note.Note()
1✔
2259
        n2 = note.Note()
1✔
2260
        n3 = note.Note()
1✔
2261
        p1.append(n1)
1✔
2262
        p1.append(n2)
1✔
2263
        p1.append(n3)
1✔
2264

2265
        slur1 = Slur()
1✔
2266
        slur1.addSpannedElements([n1, n3])
1✔
2267

2268
        p1.append(slur1)
1✔
2269

2270
        self.assertEqual(len(s), 3)
1✔
2271
        self.assertEqual(slur1.getSpannedElements(), [n1, n3])
1✔
2272

2273
        # a note can access what spanners it is part of
2274
        self.assertEqual(n1.getSpannerSites(), [slur1])
1✔
2275

2276
        # can a spanner hold spanners: yes
2277
        sl1 = Slur()
1✔
2278
        sl2 = Slur()
1✔
2279
        sl3 = Slur()
1✔
2280
        sp = Spanner([sl1, sl2, sl3])
1✔
2281
        self.assertEqual(len(sp.getSpannedElements()), 3)
1✔
2282
        self.assertEqual(sp.getSpannedElements(), [sl1, sl2, sl3])
1✔
2283

2284
        self.assertEqual(sl1.getSpannerSites(), [sp])
1✔
2285

2286
    def testSpannerAnchorRepr(self):
1✔
2287
        from music21 import stream
1✔
2288
        from music21 import spanner
1✔
2289

2290
        # SpannerAnchor with no activeSite
2291
        sa1 = spanner.SpannerAnchor()
1✔
2292
        self.assertEqual(repr(sa1), '<music21.spanner.SpannerAnchor unanchored>')
1✔
2293

2294
        # SpannerAnchor with activeSite, but no duration
2295
        m = stream.Measure()
1✔
2296
        m.insert(0.5, sa1)
1✔
2297
        self.assertEqual(repr(sa1), '<music21.spanner.SpannerAnchor at 0.5>')
1✔
2298

2299
        # SpannerAnchor with activeSite and duration
2300
        sa1.quarterLength = 2.5
1✔
2301
        self.assertEqual(repr(sa1), '<music21.spanner.SpannerAnchor at 0.5-3.0>')
1✔
2302

2303
    def testSpannerRepr(self):
1✔
2304
        from music21 import spanner
1✔
2305
        su1 = spanner.Slur()
1✔
2306
        self.assertEqual(repr(su1), '<music21.spanner.Slur>')
1✔
2307

2308
    def testSpannerFill(self):
1✔
2309
        from music21 import stream
1✔
2310
        from music21 import note
1✔
2311
        from music21 import spanner
1✔
2312
        theNotes = [note.Note('A'), note.Note('B'), note.Note('C'), note.Note('D')]
1✔
2313
        m = stream.Measure(theNotes)
1✔
2314

2315
        # Spanner with no fillElementTypes
2316
        sp = spanner.Spanner(theNotes[0], theNotes[3])
1✔
2317
        sp.fill(m)
1✔
2318
        # should not have done anything
2319
        noFillElements = [theNotes[0], theNotes[3]]
1✔
2320
        self.assertEqual(len(sp), 2)
1✔
2321
        for i, el in enumerate(sp.getSpannedElements()):
1✔
2322
            self.assertIs(el, noFillElements[i])
1✔
2323

2324
        # Ottava with filledStatus == True
2325
        ott1 = spanner.Ottava(noFillElements)
1✔
2326
        ott1.filledStatus = True  # pretend it has already been filled
1✔
2327
        ott1.fill(m)
1✔
2328
        # should not have done anything
2329
        self.assertEqual(len(sp), 2)
1✔
2330
        for i, el in enumerate(sp.getSpannedElements()):
1✔
2331
            self.assertIs(el, noFillElements[i])
1✔
2332

2333
        # same Ottava but with filledStatus == False
2334
        ott1.filledStatus = False
1✔
2335
        ott1.fill(m)
1✔
2336
        # ott1 should have been filled
2337
        self.assertIs(ott1.filledStatus, True)
1✔
2338
        self.assertEqual(len(ott1), 4)
1✔
2339
        for i, el in enumerate(ott1.getSpannedElements()):
1✔
2340
            self.assertIs(el, theNotes[i])
1✔
2341

2342
        # Ottava with no elements
2343
        ott2 = spanner.Ottava()
1✔
2344
        ott2.fill(m)
1✔
2345
        self.assertEqual(len(ott2), 0)
1✔
2346

2347
        # Ottava with only element not in searchStream
2348
        expectedElements = [note.Note('E')]
1✔
2349
        ott3 = spanner.Ottava(expectedElements)
1✔
2350
        ott3.fill(m)
1✔
2351
        self.assertEqual(len(ott3), 1)
1✔
2352
        self.assertIs(ott3.getFirst(), expectedElements[0])
1✔
2353

2354
        # Ottava with start element not in searchStream, end element is
2355
        expectedElements = [note.Note('F'), m.notes[0]]
1✔
2356
        ott4 = spanner.Ottava(expectedElements)
1✔
2357
        ott4.fill(m)
1✔
2358
        self.assertEqual(len(ott4), 2)
1✔
2359
        for i, el in enumerate(ott4.getSpannedElements()):
1✔
2360
            self.assertIs(el, expectedElements[i])
1✔
2361

2362
        # Ottava with endElement not in searchStream, startElement is
2363
        expectedElements = [m.notes[0], note.Note('G')]
1✔
2364
        ott5 = spanner.Ottava(expectedElements)
1✔
2365
        ott5.fill(m)
1✔
2366
        self.assertEqual(len(ott5), 2)
1✔
2367
        for i, el in enumerate(ott5.getSpannedElements()):
1✔
2368
            self.assertIs(el, expectedElements[i])
1✔
2369

2370
    def testSpannerBundle(self):
1✔
2371
        from music21 import spanner
1✔
2372
        from music21 import stream
1✔
2373

2374
        su1 = spanner.Slur()
1✔
2375
        su1.idLocal = 1
1✔
2376
        su2 = spanner.Slur()
1✔
2377
        su2.idLocal = 2
1✔
2378
        sb = spanner.SpannerBundle()
1✔
2379
        sb.append(su1)
1✔
2380
        sb.append(su2)
1✔
2381
        self.assertEqual(len(sb), 2)
1✔
2382
        self.assertEqual(sb[0], su1)
1✔
2383
        self.assertEqual(sb[1], su2)
1✔
2384

2385
        su3 = spanner.Slur()
1✔
2386
        su4 = spanner.Slur()
1✔
2387

2388
        s = stream.Stream()
1✔
2389
        s.append(su3)
1✔
2390
        s.append(su4)
1✔
2391
        sb2 = spanner.SpannerBundle(list(s))
1✔
2392
        self.assertEqual(len(sb2), 2)
1✔
2393
        self.assertEqual(sb2[0], su3)
1✔
2394
        self.assertEqual(sb2[1], su4)
1✔
2395

2396
    def testDeepcopySpanner(self):
1✔
2397
        from music21 import spanner
1✔
2398
        from music21 import note
1✔
2399

2400
        # how slurs might be defined
2401
        n1 = note.Note()
1✔
2402
        # n2 = note.Note()
2403
        n3 = note.Note()
1✔
2404

2405
        su1 = Slur()
1✔
2406
        su1.addSpannedElements([n1, n3])
1✔
2407

2408
        self.assertEqual(n1.getSpannerSites(), [su1])
1✔
2409
        self.assertEqual(n3.getSpannerSites(), [su1])
1✔
2410

2411
        su2 = copy.deepcopy(su1)
1✔
2412

2413
        self.assertEqual(su1.getSpannedElements(), [n1, n3])
1✔
2414
        self.assertEqual(su2.getSpannedElements(), [n1, n3])
1✔
2415

2416
        self.assertEqual(n1.getSpannerSites(), [su1, su2])
1✔
2417
        self.assertEqual(n3.getSpannerSites(), [su1, su2])
1✔
2418

2419
        sb1 = spanner.SpannerBundle([su1, su2])
1✔
2420
        sb2 = copy.deepcopy(sb1)
1✔
2421
        self.assertEqual(sb1[0].getSpannedElements(), [n1, n3])
1✔
2422
        self.assertEqual(sb2[0].getSpannedElements(), [n1, n3])
1✔
2423
        # spanners stored within are not the same objects
2424
        self.assertNotEqual(id(sb2[0]), id(sb1[0]))
1✔
2425

2426
    def testReplaceSpannedElement(self):
1✔
2427
        from music21 import note
1✔
2428
        from music21 import spanner
1✔
2429

2430
        n1 = note.Note()
1✔
2431
        n2 = note.Note()
1✔
2432
        n3 = note.Note()
1✔
2433
        n4 = note.Note()
1✔
2434
        n5 = note.Note()
1✔
2435

2436
        su1 = spanner.Slur()
1✔
2437
        su1.addSpannedElements([n1, n3])
1✔
2438

2439
        self.assertEqual(su1.getSpannedElements(), [n1, n3])
1✔
2440
        self.assertEqual(n1.getSpannerSites(), [su1])
1✔
2441

2442
        su1.replaceSpannedElement(n1, n2)
1✔
2443
        self.assertEqual(su1.getSpannedElements(), [n2, n3])
1✔
2444
        # this note now has no spanner sites
2445
        self.assertEqual(n1.getSpannerSites(), [])
1✔
2446
        self.assertEqual(n2.getSpannerSites(), [su1])
1✔
2447

2448
        # replace n2 w/ n1
2449
        su1.replaceSpannedElement(n2, n1)
1✔
2450
        self.assertEqual(su1.getSpannedElements(), [n1, n3])
1✔
2451
        self.assertEqual(n2.getSpannerSites(), [])
1✔
2452
        self.assertEqual(n1.getSpannerSites(), [su1])
1✔
2453

2454
        su2 = spanner.Slur()
1✔
2455
        su2.addSpannedElements([n3, n4])
1✔
2456

2457
        su3 = spanner.Slur()
1✔
2458
        su3.addSpannedElements([n4, n5])
1✔
2459

2460
        # n1a = note.Note()
2461
        # n2a = note.Note()
2462
        n3a = note.Note()
1✔
2463
        n4a = note.Note()
1✔
2464
        # n5a = note.Note()
2465

2466
        sb1 = spanner.SpannerBundle([su1, su2, su3])
1✔
2467
        self.assertEqual(len(sb1), 3)
1✔
2468
        self.assertEqual(list(sb1), [su1, su2, su3])
1✔
2469

2470
        # n3 is found in su1 and su2
2471

2472
        sb1.replaceSpannedElement(n3, n3a)
1✔
2473
        self.assertEqual(len(sb1), 3)
1✔
2474
        self.assertEqual(list(sb1), [su1, su2, su3])
1✔
2475

2476
        self.assertEqual(sb1[0].getSpannedElements(), [n1, n3a])
1✔
2477
        # check su2
2478
        self.assertEqual(sb1[1].getSpannedElements(), [n3a, n4])
1✔
2479

2480
        sb1.replaceSpannedElement(n4, n4a)
1✔
2481
        self.assertEqual(sb1[1].getSpannedElements(), [n3a, n4a])
1✔
2482

2483
        # check su3
2484
        self.assertEqual(sb1[2].getSpannedElements(), [n4a, n5])
1✔
2485

2486
    def testRepeatBracketA(self):
1✔
2487
        from music21 import spanner
1✔
2488
        from music21 import stream
1✔
2489

2490
        m1 = stream.Measure()
1✔
2491
        rb1 = spanner.RepeatBracket(m1)
1✔
2492
        # if added again; it is not really added, it simply is ignored
2493
        rb1.addSpannedElements(m1)
1✔
2494
        self.assertEqual(len(rb1), 1)
1✔
2495

2496
    def testRepeatBracketB(self):
1✔
2497
        from music21 import note
1✔
2498
        from music21 import spanner
1✔
2499
        from music21 import stream
1✔
2500
        from music21 import bar
1✔
2501

2502
        p = stream.Part()
1✔
2503
        m1 = stream.Measure()
1✔
2504
        m1.repeatAppend(note.Note('c4'), 4)
1✔
2505
        p.append(m1)
1✔
2506
        m2 = stream.Measure()
1✔
2507
        m2.repeatAppend(note.Note('d#4'), 4)
1✔
2508
        p.append(m2)
1✔
2509

2510
        m3 = stream.Measure()
1✔
2511
        m3.repeatAppend(note.Note('g#4'), 4)
1✔
2512
        m3.rightBarline = bar.Repeat(direction='end')
1✔
2513
        p.append(m3)
1✔
2514
        p.append(spanner.RepeatBracket(m3, number=1))
1✔
2515

2516
        m4 = stream.Measure()
1✔
2517
        m4.repeatAppend(note.Note('a4'), 4)
1✔
2518
        m4.rightBarline = bar.Repeat(direction='end')
1✔
2519
        p.append(m4)
1✔
2520
        p.append(spanner.RepeatBracket(m4, number=2))
1✔
2521

2522
        m5 = stream.Measure()
1✔
2523
        m5.repeatAppend(note.Note('b4'), 4)
1✔
2524
        m5.rightBarline = bar.Repeat(direction='end')
1✔
2525
        p.append(m5)
1✔
2526
        p.append(spanner.RepeatBracket(m5, number=3))
1✔
2527

2528
        m6 = stream.Measure()
1✔
2529
        m6.repeatAppend(note.Note('c#5'), 4)
1✔
2530
        p.append(m6)
1✔
2531

2532
        # all spanners should be at the part level
2533
        self.assertEqual(len(p.spanners), 3)
1✔
2534

2535
    # noinspection DuplicatedCode
2536
    def testRepeatBracketC(self):
1✔
2537
        from music21 import note
1✔
2538
        from music21 import spanner
1✔
2539
        from music21 import stream
1✔
2540
        from music21 import bar
1✔
2541

2542
        p = stream.Part()
1✔
2543
        m1 = stream.Measure()
1✔
2544
        m1.repeatAppend(note.Note('c4'), 4)
1✔
2545
        p.append(m1)
1✔
2546

2547
        m2 = stream.Measure()
1✔
2548
        m2.repeatAppend(note.Note('d#4'), 4)
1✔
2549
        p.append(m2)
1✔
2550

2551
        m3 = stream.Measure()
1✔
2552
        m3.repeatAppend(note.Note('g#4'), 4)
1✔
2553
        m3.rightBarline = bar.Repeat(direction='end')
1✔
2554
        p.append(m3)
1✔
2555
        rb1 = spanner.RepeatBracket(number=1)
1✔
2556
        rb1.addSpannedElements(m2, m3)
1✔
2557
        self.assertEqual(len(rb1), 2)
1✔
2558
        p.insert(0, rb1)
1✔
2559

2560
        m4 = stream.Measure()
1✔
2561
        m4.repeatAppend(note.Note('a4'), 4)
1✔
2562
        m4.rightBarline = bar.Repeat(direction='end')
1✔
2563
        p.append(m4)
1✔
2564
        p.append(spanner.RepeatBracket(m4, number=2))
1✔
2565

2566
        m5 = stream.Measure()
1✔
2567
        m5.repeatAppend(note.Note('b4'), 4)
1✔
2568
        p.append(m5)
1✔
2569

2570
        # p.show()
2571
        # all spanners should be at the part level
2572
        self.assertEqual(len(p.spanners), 2)
1✔
2573

2574
        # p.show()
2575
        raw = self.xmlStr(p)
1✔
2576
        self.assertGreater(raw.find('''<ending number="1" type="start" />'''), 1)
1✔
2577
        self.assertGreater(raw.find('''<ending number="2" type="stop" />'''), 1)
1✔
2578
        self.assertGreater(raw.find('''<ending number="2" type="start" />'''), 1)
1✔
2579

2580
    # noinspection DuplicatedCode
2581
    def testRepeatBracketD(self):
1✔
2582
        from music21 import note
1✔
2583
        from music21 import spanner
1✔
2584
        from music21 import stream
1✔
2585
        from music21 import bar
1✔
2586

2587
        p = stream.Part()
1✔
2588
        m1 = stream.Measure()
1✔
2589
        m1.repeatAppend(note.Note('c4'), 4)
1✔
2590
        p.append(m1)
1✔
2591

2592
        m2 = stream.Measure()
1✔
2593
        m2.repeatAppend(note.Note('d#4'), 4)
1✔
2594
        p.append(m2)
1✔
2595

2596
        m3 = stream.Measure()
1✔
2597
        m3.repeatAppend(note.Note('g#4'), 4)
1✔
2598
        m3.rightBarline = bar.Repeat(direction='end')
1✔
2599
        p.append(m3)
1✔
2600
        rb1 = spanner.RepeatBracket(number=1)
1✔
2601
        rb1.addSpannedElements(m2, m3)
1✔
2602
        self.assertEqual(len(rb1), 2)
1✔
2603
        p.insert(0, rb1)
1✔
2604

2605
        m4 = stream.Measure()
1✔
2606
        m4.repeatAppend(note.Note('a4'), 4)
1✔
2607
        p.append(m4)
1✔
2608

2609
        m5 = stream.Measure()
1✔
2610
        m5.repeatAppend(note.Note('b4'), 4)
1✔
2611
        m5.rightBarline = bar.Repeat(direction='end')
1✔
2612
        p.append(m5)
1✔
2613

2614
        rb2 = spanner.RepeatBracket(number=2)
1✔
2615
        rb2.addSpannedElements(m4, m5)
1✔
2616
        self.assertEqual(len(rb2), 2)
1✔
2617
        p.insert(0, rb2)
1✔
2618

2619
        m6 = stream.Measure()
1✔
2620
        m6.repeatAppend(note.Note('a4'), 4)
1✔
2621
        p.append(m6)
1✔
2622

2623
        m7 = stream.Measure()
1✔
2624
        m7.repeatAppend(note.Note('b4'), 4)
1✔
2625
        p.append(m7)
1✔
2626

2627
        m8 = stream.Measure()
1✔
2628
        m8.repeatAppend(note.Note('a4'), 4)
1✔
2629
        m8.rightBarline = bar.Repeat(direction='end')
1✔
2630
        p.append(m8)
1✔
2631

2632
        rb3 = spanner.RepeatBracket(number=3)
1✔
2633
        rb3.addSpannedElements(m6, m8)
1✔
2634
        self.assertEqual(len(rb3), 2)
1✔
2635
        p.insert(0, rb3)
1✔
2636

2637
        m9 = stream.Measure()
1✔
2638
        m9.repeatAppend(note.Note('a4'), 4)
1✔
2639
        p.append(m9)
1✔
2640

2641
        m10 = stream.Measure()
1✔
2642
        m10.repeatAppend(note.Note('b4'), 4)
1✔
2643
        p.append(m10)
1✔
2644

2645
        m11 = stream.Measure()
1✔
2646
        m11.repeatAppend(note.Note('a4'), 4)
1✔
2647
        p.append(m11)
1✔
2648

2649
        m12 = stream.Measure()
1✔
2650
        m12.repeatAppend(note.Note('a4'), 4)
1✔
2651
        m12.rightBarline = bar.Repeat(direction='end')
1✔
2652
        p.append(m12)
1✔
2653

2654
        rb4 = spanner.RepeatBracket(number=4)
1✔
2655
        rb4.addSpannedElements(m9, m10, m11, m12)
1✔
2656
        self.assertEqual(len(rb4), 4)
1✔
2657
        p.insert(0, rb4)
1✔
2658

2659
        # p.show()
2660
        # all spanners should be at the part level
2661
        self.assertEqual(len(p.getElementsByClass(stream.Measure)), 12)
1✔
2662
        self.assertEqual(len(p.spanners), 4)
1✔
2663

2664
        raw = self.xmlStr(p)
1✔
2665
        self.assertGreater(raw.find('''<ending number="1" type="start" />'''), 1)
1✔
2666
        self.assertGreater(raw.find('''<ending number="2" type="stop" />'''), 1)
1✔
2667
        self.assertGreater(raw.find('''<ending number="2" type="start" />'''), 1)
1✔
2668

2669
        p1 = copy.deepcopy(p)
1✔
2670
        raw = self.xmlStr(p1)
1✔
2671
        self.assertGreater(raw.find('''<ending number="1" type="start" />'''), 1)
1✔
2672
        self.assertGreater(raw.find('''<ending number="2" type="stop" />'''), 1)
1✔
2673
        self.assertGreater(raw.find('''<ending number="2" type="start" />'''), 1)
1✔
2674

2675
        p2 = copy.deepcopy(p1)
1✔
2676
        raw = self.xmlStr(p2)
1✔
2677
        self.assertGreater(raw.find('''<ending number="1" type="start" />'''), 1)
1✔
2678
        self.assertGreater(raw.find('''<ending number="2" type="stop" />'''), 1)
1✔
2679
        self.assertGreater(raw.find('''<ending number="2" type="start" />'''), 1)
1✔
2680

2681
    def testRepeatBracketE(self):
1✔
2682
        from music21 import note
1✔
2683
        from music21 import spanner
1✔
2684
        from music21 import stream
1✔
2685
        from music21 import bar
1✔
2686

2687
        p = stream.Part()
1✔
2688
        m1 = stream.Measure(number=1)
1✔
2689
        m1.repeatAppend(note.Note('c4'), 1)
1✔
2690
        p.append(m1)
1✔
2691
        m2 = stream.Measure(number=2)
1✔
2692
        m2.repeatAppend(note.Note('d#4'), 1)
1✔
2693
        p.append(m2)
1✔
2694

2695
        m3 = stream.Measure(number=3)
1✔
2696
        m3.repeatAppend(note.Note('g#4'), 1)
1✔
2697
        m3.rightBarline = bar.Repeat(direction='end')
1✔
2698
        p.append(m3)
1✔
2699
        p.append(spanner.RepeatBracket(m3, number=1))
1✔
2700

2701
        m4 = stream.Measure(number=4)
1✔
2702
        m4.repeatAppend(note.Note('a4'), 1)
1✔
2703
        m4.rightBarline = bar.Repeat(direction='end')
1✔
2704
        p.append(m4)
1✔
2705
        p.append(spanner.RepeatBracket(m4, number=2))
1✔
2706

2707
        m5 = stream.Measure(number=5)
1✔
2708
        m5.repeatAppend(note.Note('b4'), 1)
1✔
2709
        m5.rightBarline = bar.Repeat(direction='end')
1✔
2710
        p.append(m5)
1✔
2711
        p.append(spanner.RepeatBracket(m5, number=3))
1✔
2712

2713
        m6 = stream.Measure(number=6)
1✔
2714
        m6.repeatAppend(note.Note('c#5'), 1)
1✔
2715
        p.append(m6)
1✔
2716

2717
        # all spanners should be at the part level
2718
        self.assertEqual(len(p.spanners), 3)
1✔
2719

2720
        # try copying once
2721
        p1 = copy.deepcopy(p)
1✔
2722
        self.assertEqual(len(p1.spanners), 3)
1✔
2723
        m5 = p1.getElementsByClass(stream.Measure)[-2]
1✔
2724
        sp3 = p1.spanners[2]
1✔
2725
        self.assertTrue(sp3.hasSpannedElement(m5))
1✔
2726
        # for m in p1.getElementsByClass(stream.Measure):
2727
        #     print(m, id(m))
2728
        # for sp in p1.spanners:
2729
        #     print(sp, id(sp), [c for c in sp.getSpannedElementIds()])
2730
        # p1.show()
2731

2732
        p2 = copy.deepcopy(p1)
1✔
2733
        self.assertEqual(len(p2.spanners), 3)
1✔
2734
        m5 = p2.getElementsByClass(stream.Measure)[-2]
1✔
2735
        sp3 = p2.spanners[2]
1✔
2736
        self.assertTrue(sp3.hasSpannedElement(m5))
1✔
2737

2738
        p3 = copy.deepcopy(p2)
1✔
2739
        self.assertEqual(len(p3.spanners), 3)
1✔
2740
        m5 = p3.getElementsByClass(stream.Measure)[-2]
1✔
2741
        sp3 = p3.spanners[2]
1✔
2742
        self.assertTrue(sp3.hasSpannedElement(m5))
1✔
2743

2744
    def testOttavaShiftA(self):
1✔
2745
        '''
2746
        Test basic octave shift creation and output, as well as passing
2747
        objects through make measure calls.
2748
        '''
2749
        from music21 import stream
1✔
2750
        from music21 import note
1✔
2751
        from music21 import chord
1✔
2752
        from music21.spanner import Ottava   # need to do it this way for classSet
1✔
2753
        s = stream.Stream()
1✔
2754
        s.repeatAppend(chord.Chord(['c-3', 'g4']), 12)
1✔
2755
        # s.repeatAppend(note.Note(), 12)
2756
        n1 = s.notes[0]
1✔
2757
        n2 = s.notes[-1]
1✔
2758
        sp1 = Ottava(n1, n2)  # default is 8va
1✔
2759
        s.append(sp1)
1✔
2760
        raw = self.xmlStr(s)
1✔
2761
        self.assertEqual(raw.count('octave-shift'), 2)
1✔
2762
        self.assertEqual(raw.count('type="down"'), 1)
1✔
2763
        # s.show()
2764

2765
        s = stream.Stream()
1✔
2766
        s.repeatAppend(note.Note(), 12)
1✔
2767
        n1 = s.notes[0]
1✔
2768
        n2 = s.notes[-1]
1✔
2769
        sp1 = Ottava(n1, n2, type='8vb')
1✔
2770
        s.append(sp1)
1✔
2771
        # s.show()
2772
        raw = self.xmlStr(s)
1✔
2773
        self.assertEqual(raw.count('octave-shift'), 2)
1✔
2774
        self.assertEqual(raw.count('type="up"'), 1)
1✔
2775

2776
        s = stream.Stream()
1✔
2777
        s.repeatAppend(note.Note(), 12)
1✔
2778
        n1 = s.notes[0]
1✔
2779
        n2 = s.notes[-1]
1✔
2780
        sp1 = Ottava(n1, n2, type='15ma')
1✔
2781
        s.append(sp1)
1✔
2782
        # s.show()
2783
        raw = self.xmlStr(s)
1✔
2784
        self.assertEqual(raw.count('octave-shift'), 2)
1✔
2785
        self.assertEqual(raw.count('type="down"'), 1)
1✔
2786

2787
        s = stream.Stream()
1✔
2788
        s.repeatAppend(note.Note(), 12)
1✔
2789
        n1 = s.notes[0]
1✔
2790
        n2 = s.notes[-1]
1✔
2791
        sp1 = Ottava(n1, n2, type='15mb')
1✔
2792
        s.append(sp1)
1✔
2793
        # s.show()
2794
        raw = self.xmlStr(s)
1✔
2795
        self.assertEqual(raw.count('octave-shift'), 2)
1✔
2796
        self.assertEqual(raw.count('type="up"'), 1)
1✔
2797

2798
    def testOttavaShiftB(self):
1✔
2799
        '''
2800
        Test a single note octave
2801
        '''
2802
        from music21 import stream
1✔
2803
        from music21 import note
1✔
2804
        from music21 import spanner
1✔
2805
        s = stream.Stream()
1✔
2806
        n = note.Note('c4')
1✔
2807
        sp = spanner.Ottava(n)
1✔
2808
        s.append(n)
1✔
2809
        s.append(sp)
1✔
2810
        # s.show()
2811
        raw = self.xmlStr(s)
1✔
2812
        self.assertEqual(raw.count('octave-shift'), 2)
1✔
2813
        self.assertEqual(raw.count('type="down"'), 1)
1✔
2814

2815
    def testCrescendoA(self):
1✔
2816
        from music21 import stream
1✔
2817
        from music21 import note
1✔
2818
        from music21 import dynamics
1✔
2819
        s = stream.Stream()
1✔
2820
        # n1 = note.Note('C')
2821
        # n2 = note.Note('D')
2822
        # n3 = note.Note('E')
2823
        #
2824
        # s.append(n1)
2825
        # s.append(note.Note('A'))
2826
        # s.append(n2)
2827
        # s.append(note.Note('B'))
2828
        # s.append(n3)
2829

2830
        # s.repeatAppend(chord.Chord(['c-3', 'g4']), 12)
2831
        s.repeatAppend(note.Note(type='half'), 4)
1✔
2832
        n1 = s.notes[0]
1✔
2833
        n1.pitch.step = 'D'
1✔
2834
        # s.insert(n1.offset, dynamics.Dynamic('fff'))
2835
        n2 = s.notes[len(s.notes) // 2]
1✔
2836
        n2.pitch.step = 'E'
1✔
2837
        # s.insert(n2.offset, dynamics.Dynamic('ppp'))
2838
        n3 = s.notes[-1]
1✔
2839
        n3.pitch.step = 'F'
1✔
2840
        # s.insert(n3.offset, dynamics.Dynamic('ff'))
2841
        sp1 = dynamics.Diminuendo(n1, n2)
1✔
2842
        sp2 = dynamics.Crescendo(n2, n3)
1✔
2843
        s.append(sp1)
1✔
2844
        s.append(sp2)
1✔
2845
        # s._reprText()
2846
        # s.show('t')
2847
        raw = self.xmlStr(s)
1✔
2848
        # print(raw)
2849
        self.assertEqual(raw.count('<wedge'), 4)
1✔
2850

2851
        # self.assertEqual(raw.count('octave-shift'), 2)
2852

2853
    def testLineA(self):
1✔
2854
        from music21 import stream
1✔
2855
        from music21 import note
1✔
2856
        from music21 import spanner
1✔
2857

2858
        s = stream.Stream()
1✔
2859
        s.repeatAppend(note.Note(), 12)
1✔
2860
        n1 = s.notes[0]
1✔
2861
        n2 = s.notes[len(s.notes) // 2]
1✔
2862
        n3 = s.notes[-1]
1✔
2863
        sp1 = spanner.Line(n1, n2, startTick='up', lineType='dotted')
1✔
2864
        sp2 = spanner.Line(n2, n3, startTick='down', lineType='dashed',
1✔
2865
                                    endHeight=40)
2866
        s.append(sp1)
1✔
2867
        s.append(sp2)
1✔
2868
        # s.show('t')
2869
        raw = self.xmlStr(s)
1✔
2870
        # print(raw)
2871
        self.assertEqual(raw.count('<bracket'), 4)
1✔
2872

2873
    def testLineB(self):
1✔
2874
        from music21 import stream
1✔
2875
        from music21 import note
1✔
2876
        from music21 import spanner
1✔
2877

2878
        s = stream.Stream()
1✔
2879
        s.repeatAppend(note.Note(), 12)
1✔
2880
        n1 = s.notes[4]
1✔
2881
        n2 = s.notes[-1]
1✔
2882

2883
        n3 = s.notes[0]
1✔
2884
        n4 = s.notes[2]
1✔
2885

2886
        sp1 = spanner.Line(n1, n2, startTick='up', endTick='down', lineType='solid')
1✔
2887
        sp2 = spanner.Line(n3, n4, startTick='arrow', endTick='none', lineType='solid')
1✔
2888

2889
        s.append(sp1)
1✔
2890
        s.append(sp2)
1✔
2891

2892
        # s.show()
2893
        raw = self.xmlStr(s)
1✔
2894
        self.assertEqual(raw.count('<bracket'), 4)
1✔
2895
        self.assertEqual(raw.count('line-end="arrow"'), 1)
1✔
2896
        self.assertEqual(raw.count('line-end="none"'), 1)
1✔
2897
        self.assertEqual(raw.count('line-end="up"'), 1)
1✔
2898
        self.assertEqual(raw.count('line-end="down"'), 1)
1✔
2899

2900
    def testGlissandoA(self):
1✔
2901
        from music21 import stream
1✔
2902
        from music21 import note
1✔
2903
        from music21 import spanner
1✔
2904

2905
        s = stream.Stream()
1✔
2906
        s.repeatAppend(note.Note(), 3)
1✔
2907
        for i, n in enumerate(s.notes):
1✔
2908
            n.transpose(i + (i % 2 * 12), inPlace=True)
1✔
2909

2910
        # note: this does not support glissandi between non-adjacent notes
2911
        n1 = s.notes[0]
1✔
2912
        n2 = s.notes[len(s.notes) // 2]
1✔
2913
        n3 = s.notes[-1]
1✔
2914
        sp1 = spanner.Glissando(n1, n2)
1✔
2915
        sp2 = spanner.Glissando(n2, n3)
1✔
2916
        sp2.lineType = 'dashed'
1✔
2917
        s.append(sp1)
1✔
2918
        s.append(sp2)
1✔
2919
        s = s.makeNotation()
1✔
2920
        # s.show('t')
2921
        raw = self.xmlStr(s)
1✔
2922
        # print(raw)
2923
        self.assertEqual(raw.count('<glissando'), 4)
1✔
2924
        self.assertEqual(raw.count('line-type="dashed"'), 2)
1✔
2925

2926
    def testGlissandoB(self):
1✔
2927
        from music21 import stream
1✔
2928
        from music21 import note
1✔
2929
        from music21 import spanner
1✔
2930

2931
        s = stream.Stream()
1✔
2932
        s.repeatAppend(note.Note(), 12)
1✔
2933
        for i, n in enumerate(s.notes):
1✔
2934
            n.transpose(i + (i % 2 * 12), inPlace=True)
1✔
2935

2936
        # note: this does not support glissandi between non-adjacent notes
2937
        n1 = s.notes[0]
1✔
2938
        n2 = s.notes[1]
1✔
2939
        sp1 = spanner.Glissando(n1, n2)
1✔
2940
        sp1.lineType = 'solid'
1✔
2941
        sp1.label = 'gliss.'
1✔
2942
        s.append(sp1)
1✔
2943

2944
        # s.show()
2945
        raw = self.xmlStr(s)
1✔
2946
        self.assertEqual(raw.count('<glissando'), 2)
1✔
2947
        self.assertEqual(raw.count('line-type="solid"'), 2)
1✔
2948
        self.assertEqual(raw.count('>gliss.<'), 1)
1✔
2949

2950
    # def testDashedLineA(self):
2951
    #     from music21 import stream, note, spanner, chord, dynamics
2952
    #     s = stream.Stream()
2953
    #     s.repeatAppend(note.Note(), 12)
2954
    #     for i, n in enumerate(s.notes):
2955
    #         n.transpose(i + (i % 2 * 12), inPlace=True)
2956
    #
2957
    #     # note: Musedata presently does not support these
2958
    #     n1 = s.notes[0]
2959
    #     n2 = s.notes[len(s.notes) // 2]
2960
    #     n3 = s.notes[-1]
2961
    #     sp1 = spanner.DashedLine(n1, n2)
2962
    #     sp2 = spanner.DashedLine(n2, n3)
2963
    #     s.append(sp1)
2964
    #     s.append(sp2)
2965
    #     raw = s.musicxml
2966
    #     self.assertEqual(raw.count('<dashes'), 4)
2967

2968
    def testOneElementSpanners(self):
1✔
2969
        from music21 import note
1✔
2970
        from music21.spanner import Spanner
1✔
2971

2972
        n1 = note.Note()
1✔
2973
        sp = Spanner()
1✔
2974
        sp.addSpannedElements(n1)
1✔
2975
        sp.completeStatus = True
1✔
2976
        self.assertTrue(sp.completeStatus)
1✔
2977
        self.assertTrue(sp.isFirst(n1))
1✔
2978
        self.assertTrue(sp.isLast(n1))
1✔
2979

2980
    def testRemoveSpanners(self):
1✔
2981
        from music21 import stream
1✔
2982
        from music21 import note
1✔
2983
        from music21.spanner import Spanner, Slur
1✔
2984

2985
        p = stream.Part()
1✔
2986
        m1 = stream.Measure()
1✔
2987
        m2 = stream.Measure()
1✔
2988
        m1.number = 1
1✔
2989
        m2.number = 2
1✔
2990
        n1 = note.Note('C#4', type='whole')
1✔
2991
        n2 = note.Note('D#4', type='whole')
1✔
2992
        m1.insert(0, n1)
1✔
2993
        m2.insert(0, n2)
1✔
2994
        p.append(m1)
1✔
2995
        p.append(m2)
1✔
2996
        sl = Slur([n1, n2])
1✔
2997
        p.insert(0, sl)
1✔
2998
        for x in p:
1✔
2999
            if isinstance(x, Spanner):
1✔
3000
                p.remove(x)
1✔
3001
        self.assertEqual(len(p.spanners), 0)
1✔
3002

3003
    def testFreezeSpanners(self):
1✔
3004
        from music21 import stream
1✔
3005
        from music21 import note
1✔
3006
        from music21 import converter
1✔
3007
        from music21.spanner import Slur
1✔
3008

3009
        p = stream.Part()
1✔
3010
        m1 = stream.Measure()
1✔
3011
        m2 = stream.Measure()
1✔
3012
        m1.number = 1
1✔
3013
        m2.number = 2
1✔
3014
        n1 = note.Note('C#4', type='whole')
1✔
3015
        n2 = note.Note('D#4', type='whole')
1✔
3016
        m1.insert(0, n1)
1✔
3017
        m2.insert(0, n2)
1✔
3018
        p.append(m1)
1✔
3019
        p.append(m2)
1✔
3020
        sl = Slur([n1, n2])
1✔
3021
        p.insert(0, sl)
1✔
3022
        unused_data = converter.freezeStr(p, fmt='pickle')
1✔
3023

3024
    def testDeepcopyJustSpannerAndNotes(self):
1✔
3025
        from music21 import note
1✔
3026
        from music21 import clef
1✔
3027
        from music21.spanner import Spanner
1✔
3028

3029
        n1 = note.Note('g')
1✔
3030
        n2 = note.Note('f#')
1✔
3031
        c1 = clef.AltoClef()
1✔
3032

3033
        sp1 = Spanner(n1, n2, c1)
1✔
3034
        sp2 = copy.deepcopy(sp1)
1✔
3035
        self.assertEqual(len(sp2.spannerStorage), 3)
1✔
3036
        self.assertIsNot(sp1, sp2)
1✔
3037
        self.assertIs(sp2[0], sp1[0])
1✔
3038
        self.assertIs(sp2[2], sp1[2])
1✔
3039
        self.assertIs(sp1[0], n1)
1✔
3040
        self.assertIs(sp2[0], n1)
1✔
3041

3042
    def testDeepcopySpannerInStreamNotNotes(self):
1✔
3043
        from music21 import note
1✔
3044
        from music21 import clef
1✔
3045
        from music21 import stream
1✔
3046
        from music21.spanner import Spanner
1✔
3047

3048
        n1 = note.Note('g')
1✔
3049
        n2 = note.Note('f#')
1✔
3050
        c1 = clef.AltoClef()
1✔
3051

3052
        sp1 = Spanner(n1, n2, c1)
1✔
3053
        st1 = stream.Stream()
1✔
3054
        st1.insert(0.0, sp1)
1✔
3055
        st2 = copy.deepcopy(st1)
1✔
3056

3057
        sp2 = st2.spanners[0]
1✔
3058
        self.assertEqual(len(sp2.spannerStorage), 3)
1✔
3059
        self.assertIsNot(sp1, sp2)
1✔
3060
        self.assertIs(sp2[0], sp1[0])
1✔
3061
        self.assertIs(sp2[2], sp1[2])
1✔
3062
        self.assertIs(sp1[0], n1)
1✔
3063
        self.assertIs(sp2[0], n1)
1✔
3064

3065
    def testDeepcopyNotesInStreamNotSpanner(self):
1✔
3066
        from music21 import note
1✔
3067
        from music21 import clef
1✔
3068
        from music21 import stream
1✔
3069
        from music21.spanner import Spanner
1✔
3070

3071
        n1 = note.Note('g')
1✔
3072
        n2 = note.Note('f#')
1✔
3073
        c1 = clef.AltoClef()
1✔
3074

3075
        sp1 = Spanner(n1, n2, c1)
1✔
3076
        st1 = stream.Stream()
1✔
3077
        st1.insert(0.0, n1)
1✔
3078
        st1.insert(1.0, n2)
1✔
3079
        st2 = copy.deepcopy(st1)
1✔
3080

3081
        n3 = st2.notes[0]
1✔
3082
        self.assertEqual(len(n3.getSpannerSites()), 1)
1✔
3083
        sp2 = n3.getSpannerSites()[0]
1✔
3084
        self.assertIs(sp1, sp2)
1✔
3085
        self.assertIsNot(n1, n3)
1✔
3086
        self.assertIs(sp2[2], sp1[2])
1✔
3087
        self.assertIs(sp1[0], n1)
1✔
3088
        self.assertIs(sp2[0], n1)
1✔
3089

3090
    def testDeepcopyNotesAndSpannerInStream(self):
1✔
3091
        from music21 import note
1✔
3092
        from music21 import stream
1✔
3093
        from music21.spanner import Spanner
1✔
3094

3095
        n1 = note.Note('G4')
1✔
3096
        n2 = note.Note('F#4')
1✔
3097

3098
        sp1 = Spanner(n1, n2)
1✔
3099
        st1 = stream.Stream()
1✔
3100
        st1.insert(0.0, sp1)
1✔
3101
        st1.insert(0.0, n1)
1✔
3102
        st1.insert(1.0, n2)
1✔
3103
        st2 = copy.deepcopy(st1)
1✔
3104
        n3 = st2.notes[0]
1✔
3105
        self.assertEqual(len(n3.getSpannerSites()), 1)
1✔
3106
        sp2 = n3.getSpannerSites()[0]
1✔
3107
        self.assertIsNot(sp1, sp2)
1✔
3108
        self.assertIsNot(n1, n3)
1✔
3109

3110
        sp3 = st2.spanners[0]
1✔
3111
        self.assertIs(sp2, sp3)
1✔
3112
        self.assertIs(sp1[0], n1)
1✔
3113
        self.assertIs(sp2[0], n3)
1✔
3114

3115
    def testDeepcopyStreamWithSpanners(self):
1✔
3116
        from music21 import note
1✔
3117
        from music21 import stream
1✔
3118
        from music21.spanner import Slur
1✔
3119

3120
        n1 = note.Note()
1✔
3121
        su1 = Slur((n1,))
1✔
3122
        s = stream.Stream()
1✔
3123
        s.insert(0.0, su1)
1✔
3124
        s.insert(0.0, n1)
1✔
3125
        self.assertIs(s.spanners[0].getFirst(), n1)
1✔
3126
        self.assertIs(s.notes[0].getSpannerSites()[0], su1)
1✔
3127

3128
        s2 = copy.deepcopy(s)
1✔
3129
        su2 = s2.spanners[0]
1✔
3130
        n2 = s2.notes[0]
1✔
3131
        self.assertIsNot(su2, su1)
1✔
3132
        self.assertIsNot(n2, n1)
1✔
3133
        self.assertIs(s2.spanners[0].getFirst(), n2)
1✔
3134
        self.assertIs(s2.notes[0].getSpannerSites()[0], su2)
1✔
3135
        self.assertIsNot(s.notes[0].getSpannerSites()[0], su2)
1✔
3136
        self.assertEqual(len(s2.spannerBundle), 1)
1✔
3137
        tn2 = s2.spannerBundle.getBySpannedElement(n2)
1✔
3138
        self.assertEqual(len(tn2), 1)
1✔
3139

3140
    def testGetSpannedElementIds(self):
1✔
3141
        from music21 import note
1✔
3142
        from music21.spanner import Spanner
1✔
3143

3144
        n1 = note.Note('g')
1✔
3145
        n2 = note.Note('f#')
1✔
3146
        n3 = note.Note('e')
1✔
3147
        n4 = note.Note('d-')
1✔
3148
        n5 = note.Note('c')
1✔
3149

3150
        sl = Spanner()
1✔
3151
        sl.addSpannedElements(n1)
1✔
3152
        sl.addSpannedElements(n2, n3)
1✔
3153
        sl.addSpannedElements([n4, n5])
1✔
3154
        idList = [id(n) for n in [n1, n2, n3, n4, n5]]
1✔
3155
        slList = sl.getSpannedElementIds()
1✔
3156
        self.assertEqual(idList, slList)
1✔
3157

3158
    def testHammerOnPullOff(self):
1✔
3159
        from music21 import converter
1✔
3160
        from music21.musicxml import testPrimitive
1✔
3161

3162
        s = converter.parse(testPrimitive.notations32a)
1✔
3163

3164
        num_hammer_on = len(s.flatten().getElementsByClass('HammerOn'))
1✔
3165
        num_pull_off = len(s.flatten().getElementsByClass('PullOff'))
1✔
3166
        self.assertEqual(num_hammer_on, 1)
1✔
3167
        self.assertEqual(num_pull_off, 1)
1✔
3168

3169
        hammer_on = s.flatten().getElementsByClass('HammerOn')[0]
1✔
3170
        hammer_n0 = hammer_on.getSpannedElements()[0]
1✔
3171
        hammer_n1 = hammer_on.getSpannedElements()[1]
1✔
3172
        self.assertEqual(hammer_n0.getSpannerSites()[0], hammer_on)
1✔
3173
        self.assertEqual(hammer_n1.getSpannerSites()[0], hammer_on)
1✔
3174

3175
        pull_off = s.flatten().getElementsByClass('PullOff')[0]
1✔
3176
        pull_n0 = pull_off.getSpannedElements()[0]
1✔
3177
        pull_n1 = pull_off.getSpannedElements()[1]
1✔
3178
        self.assertEqual(pull_n0.getSpannerSites()[0], pull_off)
1✔
3179
        self.assertEqual(pull_n1.getSpannerSites()[0], pull_off)
1✔
3180

3181

3182
# ------------------------------------------------------------------------------
3183
# define presented order in documentation
3184
_DOC_ORDER: list[type] = [Spanner]
1✔
3185

3186

3187
if __name__ == '__main__':
3188
    import music21
3189
    music21.mainTest(Test)
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