From cfa44e792df653219e51a03c74b9b80251ce2e95 Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Sat, 26 Jul 2025 11:20:30 -0700 Subject: [PATCH 1/8] New features in spanner.py: (1) Let pending spanner element assignments have an associated required offset and clientInfo (if not specified, behaves as before). (2) new API insertFirstSpannedElement (3) new API popPendingSpannedElements. Also a fix that saves/restores spanner element offset/activeSite around operations that clear them. --- music21/_version.py | 2 +- music21/base.py | 2 +- music21/spanner.py | 157 ++++++++++++++++++++++++++++++++++++-------- 3 files changed, 131 insertions(+), 30 deletions(-) diff --git a/music21/_version.py b/music21/_version.py index 8f0c843b09..0eef93db2a 100644 --- a/music21/_version.py +++ b/music21/_version.py @@ -50,7 +50,7 @@ ''' from __future__ import annotations -__version__ = '9.7.1' +__version__ = '9.7.2a5' def get_version_tuple(vv): v = vv.split('.') diff --git a/music21/base.py b/music21/base.py index dcddb37f4a..fb27764d0c 100644 --- a/music21/base.py +++ b/music21/base.py @@ -27,7 +27,7 @@ >>> music21.VERSION_STR -'9.7.1' +'9.7.2a5' Alternatively, after doing a complete import, these classes are available under the module "base": diff --git a/music21/spanner.py b/music21/spanner.py index 0b41559dfc..cf2e66e72b 100644 --- a/music21/spanner.py +++ b/music21/spanner.py @@ -34,6 +34,8 @@ from music21 import prebase from music21 import sites from music21 import style +if t.TYPE_CHECKING: + from music21 import stream environLocal = environment.Environment('spanner') @@ -471,6 +473,37 @@ def addSpannedElements( self.spannerStorage.coreElementsChanged() + def insertFirstSpannedElement(self, firstEl: base.Music21Object): + ''' + Add a single element as the first in the spanner. + + >>> n1 = note.Note('g') + >>> n2 = note.Note('f#') + >>> n3 = note.Note('e') + >>> n4 = note.Note('d-') + >>> n5 = note.Note('c') + + >>> sl = spanner.Spanner() + >>> sl.addSpannedElements(n2, n3) + >>> sl.addSpannedElements([n4, n5]) + >>> sl.insertFirstSpannedElement(n1) + >>> sl.getSpannedElementIds() == [id(n) for n in [n1, n2, n3, n4, n5]] + True + ''' + origNumElements: int = len(self) + self.addSpannedElements(firstEl) + + if origNumElements == 0: + # no need to move to first element, it's already there + return + + # now move it from last to first element (if it is not last element, + # it was already in the spanner, and this API is a no-op). + if self.spannerStorage.elements[-1] is firstEl: + self.spannerStorage.elements = ( + (firstEl,) + self.spannerStorage.elements[:-1] + ) + def hasSpannedElement(self, spannedElement: base.Music21Object) -> bool: ''' Return True if this Spanner has the spannedElement. @@ -609,13 +642,14 @@ def fill( ) if t.TYPE_CHECKING: - from music21 import stream assert isinstance(searchStream, stream.Stream) endElement: base.Music21Object|None = self.getLast() if endElement is startElement: endElement = None + savedEndElementOffset: OffsetQL | None = None + savedEndElementActiveSite: stream.Stream | None = None if endElement is not None: # Start and end elements are different; we can't just append everything, we need # to save the end element, remove it, add everything, then add the end element @@ -623,6 +657,11 @@ def fill( # filling, the new intermediate elements will come after the existing ones, # regardless of offset. But first and last will still be the same two elements # as before, which is the most important thing. + + # But doing this (remove/restore) clears endElement.offset and endElement.activeSite. + # That's rude; put 'em back when we're done. + savedEndElementOffset = endElement.offset + savedEndElementActiveSite = endElement.activeSite self.spannerStorage.remove(endElement) try: @@ -631,6 +670,10 @@ def fill( # print('start element not in searchStream') if endElement is not None: self.addSpannedElements(endElement) + if savedEndElementOffset is not None: + endElement.offset = savedEndElementOffset + if savedEndElementActiveSite is not None: + endElement.activeSite = savedEndElementActiveSite return endOffsetInHierarchy: OffsetQL @@ -642,6 +685,10 @@ def fill( except sites.SitesException: # print('end element not in searchStream') self.addSpannedElements(endElement) + if savedEndElementOffset is not None: + endElement.offset = savedEndElementOffset + if savedEndElementActiveSite is not None: + endElement.activeSite = savedEndElementActiveSite return else: endOffsetInHierarchy = ( @@ -672,6 +719,10 @@ def fill( if endElement is not None: # add it back in as the end element self.addSpannedElements(endElement) + if savedEndElementOffset is not None: + endElement.offset = savedEndElementOffset + if savedEndElementActiveSite is not None: + endElement.activeSite = savedEndElementActiveSite self.filledStatus = True @@ -752,10 +803,17 @@ def getLast(self): # ------------------------------------------------------------------------------ -class _SpannerRef(t.TypedDict): +class PendingAssignmentRef(t.TypedDict): + ''' + An object containing information about a pending first spanned element + assignment. See setPendingFirstSpannedElementAssignment for documentation + and tests. + ''' # noinspection PyTypedDict spanner: 'Spanner' className: str + offsetInScore: OffsetQL|None + clientInfo: t.Any|None class SpannerAnchor(base.Music21Object): ''' @@ -799,14 +857,21 @@ def __init__(self, **keywords): super().__init__(**keywords) def _reprInternal(self) -> str: + offset: OffsetQL = self.offset if self.activeSite is None: - return 'unanchored' + # find a site that is either a Measure or a Voice + siteList: list = self.sites.getSitesByClass('Measure') + if not siteList: + siteList = self.sites.getSitesByClass('Voice') + if not siteList: + return 'unanchored' + offset = self.getOffsetInHierarchy(siteList[0]) ql: OffsetQL = self.duration.quarterLength if ql == 0: - return f'at {self.offset}' + return f'at {offset}' - return f'at {self.offset}-{self.offset + ql}' + return f'at {offset}-{offset + ql}' class SpannerBundle(prebase.ProtoM21Object): @@ -839,10 +904,10 @@ def __init__(self, spanners: list[Spanner]|None = None): self._storage = spanners[:] # a simple List, not a Stream # special spanners, stored in storage, can be identified in the - # SpannerBundle as missing a spannedElement; the next obj that meets + # SpannerBundle as missing a first spannedElement; the next obj that meets # the class expectation will then be assigned and the spannedElement # cleared - self._pendingSpannedElementAssignment: list[_SpannerRef] = [] + self._pendingSpannedElementAssignment: list[PendingAssignmentRef] = [] def append(self, other: Spanner): ''' @@ -1253,16 +1318,22 @@ def setPendingSpannedElementAssignment( self, sp: Spanner, className: str, + offsetInScore: OffsetQL|None = None, + clientInfo: t.Any|None = None ): ''' - A SpannerBundle can be set up so that a particular spanner (sp) - is looking for an element of class (className) to complete it. Any future - element that matches the className which is passed to the SpannerBundle - via freePendingSpannedElementAssignment() will get it. + A SpannerBundle can be set up so that a particular spanner (sp) is looking + for an element of class (className) to be set as first element. Any future + future element that matches the className (and offsetInScore, if specified) + which is passed to the SpannerBundle via freePendingFirstSpannedElementAssignment() + will get it. clientInfo is not used in the match, but can be used by the client + when cleaning up any leftover pending assignments, by creating SpannerAnchors + at the appropriate offset. >>> n1 = note.Note('C') >>> r1 = note.Rest() >>> n2 = note.Note('D') + >>> n2Wrong = note.Note('B') >>> n3 = note.Note('E') >>> su1 = spanner.Slur([n1]) >>> sb = spanner.SpannerBundle() @@ -1275,44 +1346,60 @@ def setPendingSpannedElementAssignment( Now set up su1 to get the next note assigned to it. - >>> sb.setPendingSpannedElementAssignment(su1, 'Note') + >>> sb.setPendingSpannedElementAssignment(su1, 'Note', 0.) Call freePendingSpannedElementAssignment to attach. + Should not get a note at the wrong offset. + + >>> sb.freePendingSpannedElementAssignment(n2Wrong, 1.) + >>> su1.getSpannedElements() + [] + Should not get a rest, because it is not a 'Note' - >>> sb.freePendingSpannedElementAssignment(r1) + >>> sb.freePendingSpannedElementAssignment(r1, 0.) >>> su1.getSpannedElements() [] But will get the next note: - >>> sb.freePendingSpannedElementAssignment(n2) + >>> sb.freePendingSpannedElementAssignment(n2, 0.) >>> su1.getSpannedElements() - [, ] + [, ] >>> n2.getSpannerSites() - [>] + [>] And now that the assignment has been made, the pending assignment has been cleared, so n3 will not get assigned to the slur: - >>> sb.freePendingSpannedElementAssignment(n3) + >>> sb.freePendingSpannedElementAssignment(n3, 0.) >>> su1.getSpannedElements() - [, ] + [, ] >>> n3.getSpannerSites() [] ''' - ref: _SpannerRef = {'spanner': sp, 'className': className} + ref: PendingAssignmentRef = { + 'spanner': sp, + 'className': className, + 'offsetInScore': offsetInScore, + 'clientInfo': clientInfo + } self._pendingSpannedElementAssignment.append(ref) - def freePendingSpannedElementAssignment(self, spannedElementCandidate): + def freePendingSpannedElementAssignment( + self, + spannedElementCandidate, + offsetInScore: OffsetQL|None = None + ): ''' - Assigns and frees up a pendingSpannedElementAssignment if one is - active and the candidate matches the class. See - setPendingSpannedElementAssignment for documentation and tests. + Assigns and frees up a pendingSpannedElementAssignment if one + is active and the candidate matches the class (and offsetInScore, + if specified). See setPendingSpannedElementAssignment for + documentation and tests. It is set up via a first-in, first-out priority. ''' @@ -1325,14 +1412,28 @@ def freePendingSpannedElementAssignment(self, spannedElementCandidate): # environLocal.printDebug(['calling freePendingSpannedElementAssignment()', # self._pendingSpannedElementAssignment]) if ref['className'] in spannedElementCandidate.classSet: - ref['spanner'].addSpannedElements(spannedElementCandidate) - remove = i - # environLocal.printDebug(['freePendingSpannedElementAssignment()', - # 'added spannedElement', ref['spanner']]) - break + if (offsetInScore is None + or offsetInScore == ref['offsetInScore']): + ref['spanner'].insertFirstSpannedElement(spannedElementCandidate) + remove = i + # environLocal.printDebug(['freePendingSpannedElementAssignment()', + # 'added spannedElement', ref['spanner']]) + break if remove is not None: self._pendingSpannedElementAssignment.pop(remove) + def popPendingSpannedElementAssignments(self) -> list[PendingAssignmentRef]: + ''' + Removes and returns all pendingSpannedElementAssignments. + This can be called when there will be no more calls to + freePendingSpannedElementAssignment, and SpannerAnchors + need to be created for each remaining pending assignment. + The SpannerAnchors should be created at the appropriate + offset, dictated by the assignment's offsetInScore. + ''' + output: list[PendingAssignmentRef] = self._pendingSpannedElementAssignment + self._pendingSpannedElementAssignment = [] + return output # ------------------------------------------------------------------------------ # connect two or more notes anywhere in the score From ac88879089d11ca2c6202e02dfe36f9f6b345dfd Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:36:30 -0700 Subject: [PATCH 2/8] Add some tests. --- music21/spanner.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/music21/spanner.py b/music21/spanner.py index cf2e66e72b..56b485a275 100644 --- a/music21/spanner.py +++ b/music21/spanner.py @@ -1430,6 +1430,24 @@ def popPendingSpannedElementAssignments(self) -> list[PendingAssignmentRef]: need to be created for each remaining pending assignment. The SpannerAnchors should be created at the appropriate offset, dictated by the assignment's offsetInScore. + + >>> sb = spanner.SpannerBundle() + >>> sl = spanner.Slur() + >>> sb.append(sl) + >>> sb.setPendingSpannedElementAssignment(sl, 'Note', 0.) + + Check to make sure popPendingSpannedElementAssignments returns + the entire list, and leaves an empty list behind. + >>> expectedPending = sb._pendingSpannedElementAssignment + >>> expectedPending + [{'spanner': , 'className': 'Note', + 'offsetInScore': 0.0, 'clientInfo': None}] + + >>> pending = sb.popPendingSpannedElementAssignments() + >>> pending == expectedPending + True + >>> sb._pendingSpannedElementAssignment + [] ''' output: list[PendingAssignmentRef] = self._pendingSpannedElementAssignment self._pendingSpannedElementAssignment = [] From 197a11492619e30f64be671301d42902427e3dc7 Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:53:42 -0700 Subject: [PATCH 3/8] Better demonstration of old and new use of the PendingSpannedElementAssignment APIs. --- music21/spanner.py | 113 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 93 insertions(+), 20 deletions(-) diff --git a/music21/spanner.py b/music21/spanner.py index 56b485a275..b4ec71fc7c 100644 --- a/music21/spanner.py +++ b/music21/spanner.py @@ -810,7 +810,7 @@ class PendingAssignmentRef(t.TypedDict): and tests. ''' # noinspection PyTypedDict - spanner: 'Spanner' + spanner: Spanner className: str offsetInScore: OffsetQL|None clientInfo: t.Any|None @@ -1330,57 +1330,130 @@ def setPendingSpannedElementAssignment( when cleaning up any leftover pending assignments, by creating SpannerAnchors at the appropriate offset. + There are two ways to use the PendingSpannedElement APIs. The old way, + where setPendingSpannedElementAssignment is called without specifying + offsetInScore or clientInfo, and freePendingSpannedElementAssignment + is called without specifying a matching offset; and the new way, where + setPendingSpannedElementAssignment is called with an offsetInScore (and + perhaps a clientInfo), freePendingSpannedElementAssignment is called + with a matching offset, and then popPendingSpannedElementAssignments is + called to get all the remaining pending assignments, so that SpannerAnchors + can be created for them (since there was no note found at the specified + offsetInScore). clientInfo is an optional argument in the new-style + API call, to stash off any info that is needed for the calling client + to correctly create and place the SpannerAnchors. This can be as simple + as an int, or as complex as the complete client object. + + The new way is useful (for example) for importing a from + MusicXML that has specified, so that the next note parsed after + the will not be at the correct offsetInScore for the start + of the direction, and a SpannerAnchor will be required instead. + + Test the old way (no offsetInScore or clientInfo): + >>> n1 = note.Note('C') >>> r1 = note.Rest() >>> n2 = note.Note('D') - >>> n2Wrong = note.Note('B') >>> n3 = note.Note('E') - >>> su1 = spanner.Slur([n1]) + >>> su1 = spanner.Slur() >>> sb = spanner.SpannerBundle() >>> sb.append(su1) >>> su1.getSpannedElements() - [] + [] >>> n1.getSpannerSites() - [>] + [] Now set up su1 to get the next note assigned to it. - >>> sb.setPendingSpannedElementAssignment(su1, 'Note', 0.) + >>> sb.setPendingSpannedElementAssignment(su1, 'Note') Call freePendingSpannedElementAssignment to attach. - Should not get a note at the wrong offset. - - >>> sb.freePendingSpannedElementAssignment(n2Wrong, 1.) - >>> su1.getSpannedElements() - [] - Should not get a rest, because it is not a 'Note' - >>> sb.freePendingSpannedElementAssignment(r1, 0.) + >>> sb.freePendingSpannedElementAssignment(r1) >>> su1.getSpannedElements() - [] + [] But will get the next note: - >>> sb.freePendingSpannedElementAssignment(n2, 0.) + >>> sb.freePendingSpannedElementAssignment(n1) >>> su1.getSpannedElements() - [, ] + [] - >>> n2.getSpannerSites() - [>] + >>> n1.getSpannerSites() + [>] And now that the assignment has been made, the pending assignment has been cleared, so n3 will not get assigned to the slur: - >>> sb.freePendingSpannedElementAssignment(n3, 0.) + >>> sb.freePendingSpannedElementAssignment(n3) >>> su1.getSpannedElements() - [, ] + [] >>> n3.getSpannerSites() [] + And now we encounter the end of the and put the most recently parsed + note in the spanner. + + >>> su1.addSpannedElements(n2) + >>> su1.getSpannedElements() + [, ] + + >>> n2.getSpannerSites() + [>] + + Test the new way (offsetInScore specified): + + >>> n1 = note.Note('C') + >>> r1 = note.Rest() + >>> n2Wrong = note.Note('B') + >>> n3 = note.Note('E') + >>> su1 = spanner.Slur([n1]) + >>> sb = spanner.SpannerBundle() + >>> sb.append(su1) + >>> su1.getSpannedElements() + [] + + >>> n1.getSpannerSites() + [>] + + Now set up su1 to get the next note assigned to it. Stash off (in + clientInfo) the staffKey (1) that should be used for the SpannerAnchor, + should a SpannerAnchor be needed. + + >>> sb.setPendingSpannedElementAssignment(su1, 'Note', 0.0, clientInfo=1) + + Call freePendingSpannedElementAssignment to attach. + Should not get a note at the wrong offset. + + >>> sb.freePendingSpannedElementAssignment(n2Wrong, 1.0) + >>> su1.getSpannedElements() + [] + + Should not get a rest, because it is not a 'Note' + + >>> sb.freePendingSpannedElementAssignment(r1, 0.0) + >>> su1.getSpannedElements() + [] + + >>> n1.getSpannerSites() + [>] + + Get the remaining pending assignments, so we can create SpannerAnchors for them + + >>> unmatched = sb.popPendingSpannedElementAssignments() + >>> len(unmatched) + 1 + + Here, we are instructed to create a SpannerAnchor in staff 1, at score offset 0.0 + + >>> unmatched[0] + {'spanner': >, 'className': 'Note', + 'offsetInScore': 0.0, 'clientInfo': 1} + ''' ref: PendingAssignmentRef = { 'spanner': sp, From 4b96a01bf3002c433757e8bd123daceb2f343f66 Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Thu, 14 Aug 2025 13:21:06 -0700 Subject: [PATCH 4/8] Rename "clientInfo: t.Any|None" becomes "staffKey: int|None". --- music21/spanner.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/music21/spanner.py b/music21/spanner.py index b4ec71fc7c..2f834dc762 100644 --- a/music21/spanner.py +++ b/music21/spanner.py @@ -813,7 +813,7 @@ class PendingAssignmentRef(t.TypedDict): spanner: Spanner className: str offsetInScore: OffsetQL|None - clientInfo: t.Any|None + staffKey: int|None class SpannerAnchor(base.Music21Object): ''' @@ -1319,37 +1319,36 @@ def setPendingSpannedElementAssignment( sp: Spanner, className: str, offsetInScore: OffsetQL|None = None, - clientInfo: t.Any|None = None + staffKey: int|None = None ): ''' A SpannerBundle can be set up so that a particular spanner (sp) is looking for an element of class (className) to be set as first element. Any future future element that matches the className (and offsetInScore, if specified) which is passed to the SpannerBundle via freePendingFirstSpannedElementAssignment() - will get it. clientInfo is not used in the match, but can be used by the client + will get it. staffKey is not used in the match, but can be used by the client when cleaning up any leftover pending assignments, by creating SpannerAnchors - at the appropriate offset. + in the appropriate staff. There are two ways to use the PendingSpannedElement APIs. The old way, where setPendingSpannedElementAssignment is called without specifying - offsetInScore or clientInfo, and freePendingSpannedElementAssignment + offsetInScore or staffKey, and freePendingSpannedElementAssignment is called without specifying a matching offset; and the new way, where setPendingSpannedElementAssignment is called with an offsetInScore (and - perhaps a clientInfo), freePendingSpannedElementAssignment is called + perhaps a staffKey), freePendingSpannedElementAssignment is called with a matching offset, and then popPendingSpannedElementAssignments is called to get all the remaining pending assignments, so that SpannerAnchors can be created for them (since there was no note found at the specified - offsetInScore). clientInfo is an optional argument in the new-style - API call, to stash off any info that is needed for the calling client - to correctly create and place the SpannerAnchors. This can be as simple - as an int, or as complex as the complete client object. + offsetInScore). staffKey is an optional argument in the new-style + API call, to stash off the info that is needed for the calling client + to correctly create and place the SpannerAnchors. The new way is useful (for example) for importing a from MusicXML that has specified, so that the next note parsed after the will not be at the correct offsetInScore for the start of the direction, and a SpannerAnchor will be required instead. - Test the old way (no offsetInScore or clientInfo): + Test the old way (no offsetInScore or staffKey): >>> n1 = note.Note('C') >>> r1 = note.Rest() @@ -1420,11 +1419,10 @@ def setPendingSpannedElementAssignment( >>> n1.getSpannerSites() [>] - Now set up su1 to get the next note assigned to it. Stash off (in - clientInfo) the staffKey (1) that should be used for the SpannerAnchor, - should a SpannerAnchor be needed. + Now set up su1 to get the next note assigned to it. Stash off the staffKey (1) that + should be used for the SpannerAnchor, should a SpannerAnchor be needed. - >>> sb.setPendingSpannedElementAssignment(su1, 'Note', 0.0, clientInfo=1) + >>> sb.setPendingSpannedElementAssignment(su1, 'Note', 0.0, staffKey=1) Call freePendingSpannedElementAssignment to attach. Should not get a note at the wrong offset. @@ -1452,14 +1450,14 @@ def setPendingSpannedElementAssignment( >>> unmatched[0] {'spanner': >, 'className': 'Note', - 'offsetInScore': 0.0, 'clientInfo': 1} + 'offsetInScore': 0.0, 'staffKey': 1} ''' ref: PendingAssignmentRef = { 'spanner': sp, 'className': className, 'offsetInScore': offsetInScore, - 'clientInfo': clientInfo + 'staffKey': staffKey } self._pendingSpannedElementAssignment.append(ref) @@ -1507,14 +1505,14 @@ def popPendingSpannedElementAssignments(self) -> list[PendingAssignmentRef]: >>> sb = spanner.SpannerBundle() >>> sl = spanner.Slur() >>> sb.append(sl) - >>> sb.setPendingSpannedElementAssignment(sl, 'Note', 0.) + >>> sb.setPendingSpannedElementAssignment(sl, 'Note', 0.0) Check to make sure popPendingSpannedElementAssignments returns the entire list, and leaves an empty list behind. >>> expectedPending = sb._pendingSpannedElementAssignment >>> expectedPending [{'spanner': , 'className': 'Note', - 'offsetInScore': 0.0, 'clientInfo': None}] + 'offsetInScore': 0.0, 'staffKey': None}] >>> pending = sb.popPendingSpannedElementAssignments() >>> pending == expectedPending From 79aad8c131cd3faa205913f35094a045ee137704 Mon Sep 17 00:00:00 2001 From: Greg Chapman <75333244+gregchapman-dev@users.noreply.github.com> Date: Thu, 14 Aug 2025 15:37:36 -0700 Subject: [PATCH 5/8] Change version numbers in the approved way. --- music21/_version.py | 2 +- music21/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/_version.py b/music21/_version.py index a479e115f9..7ecb8cc6e0 100644 --- a/music21/_version.py +++ b/music21/_version.py @@ -50,7 +50,7 @@ ''' from __future__ import annotations -__version__ = '9.7.2a6' +__version__ = '9.7.3' def get_version_tuple(vv): v = vv.split('.') diff --git a/music21/base.py b/music21/base.py index 9ac0a1589a..e318040edc 100644 --- a/music21/base.py +++ b/music21/base.py @@ -27,7 +27,7 @@ >>> music21.VERSION_STR -'9.7.2a6' +'9.7.3' Alternatively, after doing a complete import, these classes are available under the module "base": From fae69145c20281f646e3fc40745f5433a84f2632 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 13 May 2026 11:25:18 -1000 Subject: [PATCH 6/8] update docs --- music21/spanner.py | 183 +++++++++++++++++++++++++++------------------ 1 file changed, 111 insertions(+), 72 deletions(-) diff --git a/music21/spanner.py b/music21/spanner.py index 352fae063f..22ed7b26b3 100644 --- a/music21/spanner.py +++ b/music21/spanner.py @@ -473,7 +473,7 @@ def addSpannedElements( # ]) self.spannerStorage.coreElementsChanged() - def insertFirstSpannedElement(self, firstEl: base.Music21Object): + def insertFirstSpannedElement(self, firstEl: base.Music21Object) -> None: ''' Add a single element as the first in the spanner. @@ -489,6 +489,13 @@ def insertFirstSpannedElement(self, firstEl: base.Music21Object): >>> sl.insertFirstSpannedElement(n1) >>> sl.getSpannedElementIds() == [id(n) for n in [n1, n2, n3, n4, n5]] True + + If the element is already in the spanner, it will not be added again, nor moved. + (to move it to the front, remove it before running) + + >>> sl.insertFirstSpannedElement(n4) + >>> sl.getSpannedElementIds() == [id(n) for n in [n1, n2, n3, n4, n5]] + True ''' origNumElements: int = len(self) self.addSpannedElements(firstEl) @@ -1319,147 +1326,178 @@ def setPendingSpannedElementAssignment( sp: Spanner, className: str, offsetInScore: OffsetQL|None = None, - staffKey: int|None = None - ): + ) -> PendingAssignmentRef: ''' A SpannerBundle can be set up so that a particular spanner (sp) is looking for an element of class (className) to be set as first element. Any future future element that matches the className (and offsetInScore, if specified) which is passed to the SpannerBundle via freePendingFirstSpannedElementAssignment() - will get it. staffKey is not used in the match, but can be used by the client - when cleaning up any leftover pending assignments, by creating SpannerAnchors - in the appropriate staff. + will get it. - There are two ways to use the PendingSpannedElement APIs. The old way, + There are two ways to use the PendingSpannedElement APIs. One where setPendingSpannedElementAssignment is called without specifying - offsetInScore or staffKey, and freePendingSpannedElementAssignment - is called without specifying a matching offset; and the new way, where - setPendingSpannedElementAssignment is called with an offsetInScore (and - perhaps a staffKey), freePendingSpannedElementAssignment is called + offsetInScore and freePendingSpannedElementAssignment + is called without specifying a matching offset; and a more specific way where + setPendingSpannedElementAssignment is called with an offsetInScore, + freePendingSpannedElementAssignment is called with a matching offset, and then popPendingSpannedElementAssignments is called to get all the remaining pending assignments, so that SpannerAnchors can be created for them (since there was no note found at the specified - offsetInScore). staffKey is an optional argument in the new-style - API call, to stash off the info that is needed for the calling client - to correctly create and place the SpannerAnchors. + offsetInScore). The new way is useful (for example) for importing a from MusicXML that has specified, so that the next note parsed after the will not be at the correct offsetInScore for the start of the direction, and a SpannerAnchor will be required instead. - Test the old way (no offsetInScore or staffKey): + Usage without offset: + + Create some notes: >>> n1 = note.Note('C') >>> r1 = note.Rest() >>> n2 = note.Note('D') >>> n3 = note.Note('E') + + Notes start without any associated spanners. + + >>> n1.getSpannerSites() + [] + + Create a slur. + >>> su1 = spanner.Slur() - >>> sb = spanner.SpannerBundle() - >>> sb.append(su1) + >>> sb1 = spanner.SpannerBundle() + >>> sb1.append(su1) + + It starts with no notes. + >>> su1.getSpannedElements() [] - >>> n1.getSpannerSites() - [] - Now set up su1 to get the next note assigned to it. + Now call this method to set up su1 to get the next note assigned to the spannerBundle. + This method returns a TypedDict called a `PendingAssignmentRef` which essentially + just encodes the information passed into this function: - >>> sb.setPendingSpannedElementAssignment(su1, 'Note') + >>> sb1.setPendingSpannedElementAssignment(su1, 'Note') + {'spanner': , 'className': 'Note', + 'offsetInScore': None, 'staffKey': None} - Call freePendingSpannedElementAssignment to attach. + Elements are then potentially assigned to spanners by calling + `freePendingSpannedElementAssignment` on the spannerBundle. - Should not get a rest, because it is not a 'Note' + That method will not attach a rest like `r1` because it is not a 'Note' - >>> sb.freePendingSpannedElementAssignment(r1) + >>> sb1.freePendingSpannedElementAssignment(r1) >>> su1.getSpannedElements() [] - But will get the next note: + But `freePendingSpannedElementAssignment` will properly associate the next + Note object with the spanner. - >>> sb.freePendingSpannedElementAssignment(n1) + >>> sb1.freePendingSpannedElementAssignment(n1) + >>> su1 + > >>> su1.getSpannedElements() [] - >>> n1.getSpannerSites() [>] + >>> n1 in su1 + True And now that the assignment has been made, the pending assignment has been cleared, so n3 will not get assigned to the slur: - >>> sb.freePendingSpannedElementAssignment(n3) + >>> sb1.freePendingSpannedElementAssignment(n3) >>> su1.getSpannedElements() [] - + >>> n3 in su1 + False >>> n3.getSpannerSites() [] - And now we encounter the end of the and put the most recently parsed - note in the spanner. + And we can see that the SpannerBundle `sb1` has no spanners still awaiting (pending) + elements to assign: - >>> su1.addSpannedElements(n2) - >>> su1.getSpannedElements() - [, ] + >>> sb1.popPendingSpannedElementAssignments() + [] - >>> n2.getSpannerSites() - [>] + Now the same deal but with offsetInScore. When offsetInScore is + specified it is used to filter out the matches + by `freePendingSpannedElementAssignment`. - Test the new way (offsetInScore specified): + Create two notes and a rest. - >>> n1 = note.Note('C') - >>> r1 = note.Rest() - >>> n2Wrong = note.Note('B') - >>> n3 = note.Note('E') - >>> su1 = spanner.Slur([n1]) - >>> sb = spanner.SpannerBundle() - >>> sb.append(su1) - >>> su1.getSpannedElements() - [] + >>> n4 = note.Note('C') + >>> wrongOffsetNote = note.Note('B') + >>> r2 = note.Rest() - >>> n1.getSpannerSites() - [>] + Create a slur with n4 already in it. - Now set up su1 to get the next note assigned to it. Stash off the staffKey (1) that - should be used for the SpannerAnchor, should a SpannerAnchor be needed. + >>> sb2 = spanner.SpannerBundle() + >>> su2 = spanner.Slur([n4]) + >>> sb2.append(su2) + >>> su2.getSpannedElements() + [] - >>> sb.setPendingSpannedElementAssignment(su1, 'Note', 0.0, staffKey=1) + Now set up su2 to get the next note assigned to it at offset 4.0. - Call freePendingSpannedElementAssignment to attach. - Should not get a note at the wrong offset. + >>> ref = sb2.setPendingSpannedElementAssignment(su2, 'Note', 4.0) - >>> sb.freePendingSpannedElementAssignment(n2Wrong, 1.0) - >>> su1.getSpannedElements() - [] + We will also give `ref`, a `PendingAssignmentRef`, the staffKey of 1 + which could be used in later parsing (such as in MusicXML parsing to figure out + which PartStaff to assign the slur or SpannerAnchor to). It is unused by + `freePendingSpannedElementAssignment` - Should not get a rest, because it is not a 'Note' + >>> ref['staffKey'] = 1 + >>> ref + {'spanner': >, 'className': 'Note', + 'offsetInScore': 4.0, 'staffKey': 1} - >>> sb.freePendingSpannedElementAssignment(r1, 0.0) - >>> su1.getSpannedElements() - [] + Call `freePendingSpannedElementAssignment` to attach an element of the right class + and offset. Note that the offset must be passed to the method; it is not + necessarily the offset of the object itself (most often it is the `offsetInHierarchy` + of the Part object). - >>> n1.getSpannerSites() - [>] + >>> sb2.freePendingSpannedElementAssignment(wrongOffsetNote, 5.0) + >>> wrongOffsetNote in su2 + False - Get the remaining pending assignments, so we can create SpannerAnchors for them + Again, it will not get a rest, even at the correct offsetInScore, + because it is not the class being searched for. - >>> unmatched = sb.popPendingSpannedElementAssignments() - >>> len(unmatched) - 1 + >>> sb2.freePendingSpannedElementAssignment(r2, 4.0) + >>> r2 in su2 + False - Here, we are instructed to create a SpannerAnchor in staff 1, at score offset 0.0 + We are all out of possible notes, we can see which PendingAssignmentRef elements + are still awaiting elements to attach to. In this case, there is one, our + Slur - >>> unmatched[0] + >>> unmatched_pendingAssignmentRefs = sb2.popPendingSpannedElementAssignments() + >>> len(unmatched_pendingAssignmentRefs) + 1 + >>> unmatched_pendingAssignmentRefs[0] {'spanner': >, 'className': 'Note', - 'offsetInScore': 0.0, 'staffKey': 1} + 'offsetInScore': 4.0, 'staffKey': 1} + >>> unmatched_pendingAssignmentRefs[0]['spanner'] is su2 + True + Parsers can use the information from the unmatched PendingAssignmentRef to + add additional elements to the score at places where a match was expected. + Given this ref, the most logical choice would be to create a `SpannerAnchor` + object and put it at score offset 4.0. The manually added `staffKey` of 1, + would help a parser remember that the SpannerAnchor should go in staff 1. ''' ref: PendingAssignmentRef = { 'spanner': sp, 'className': className, 'offsetInScore': offsetInScore, - 'staffKey': staffKey + 'staffKey': None, } self._pendingSpannedElementAssignment.append(ref) + return ref def freePendingSpannedElementAssignment( self, @@ -1469,12 +1507,11 @@ def freePendingSpannedElementAssignment( ''' Assigns and frees up a pendingSpannedElementAssignment if one is active and the candidate matches the class (and offsetInScore, - if specified). See setPendingSpannedElementAssignment for + if specified). See setPendingSpannedElementAssignment for documentation and tests. It is set up via a first-in, first-out priority. ''' - if not self._pendingSpannedElementAssignment: return @@ -1506,6 +1543,8 @@ def popPendingSpannedElementAssignments(self) -> list[PendingAssignmentRef]: >>> sl = spanner.Slur() >>> sb.append(sl) >>> sb.setPendingSpannedElementAssignment(sl, 'Note', 0.0) + {'spanner': , 'className': 'Note', + 'offsetInScore': 0.0, 'staffKey': None} Check to make sure popPendingSpannedElementAssignments returns the entire list, and leaves an empty list behind. From 8d703cacb8f854785367aaf105eda8db2f4878dd Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 13 May 2026 12:04:34 -1000 Subject: [PATCH 7/8] dataclass, not typedict --- music21/spanner.py | 164 ++++++++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 78 deletions(-) diff --git a/music21/spanner.py b/music21/spanner.py index 22ed7b26b3..17f1ac21ec 100644 --- a/music21/spanner.py +++ b/music21/spanner.py @@ -21,6 +21,7 @@ from collections.abc import Sequence, Iterable import copy +from dataclasses import dataclass import typing as t import unittest @@ -810,17 +811,16 @@ def getLast(self): # ------------------------------------------------------------------------------ -class PendingAssignmentRef(t.TypedDict): +@dataclass(frozen=True) +class PendingAssignmentRef: ''' - An object containing information about a pending first spanned element - assignment. See setPendingFirstSpannedElementAssignment for documentation + A dataclass containing information about a pending first spanned element assignment. + See :meth:`SpannerBundle.setPendingSpannedElementAssignment` for documentation and tests. ''' - # noinspection PyTypedDict spanner: Spanner className: str - offsetInScore: OffsetQL|None - staffKey: int|None + offsetInScore: OffsetQL|None = None class SpannerAnchor(base.Music21Object): ''' @@ -1330,8 +1330,8 @@ def setPendingSpannedElementAssignment( ''' A SpannerBundle can be set up so that a particular spanner (sp) is looking for an element of class (className) to be set as first element. Any future - future element that matches the className (and offsetInScore, if specified) - which is passed to the SpannerBundle via freePendingFirstSpannedElementAssignment() + element that matches the className (and offsetInScore, if specified) + which is passed to the SpannerBundle via freePendingSpannedElementAssignment() will get it. There are two ways to use the PendingSpannedElement APIs. One @@ -1352,7 +1352,7 @@ def setPendingSpannedElementAssignment( Usage without offset: - Create some notes: + Create some notes and a rest. >>> n1 = note.Note('C') >>> r1 = note.Rest() @@ -1364,54 +1364,46 @@ def setPendingSpannedElementAssignment( >>> n1.getSpannerSites() [] - Create a slur. + Create an empty Slur and a SpannerBundle to hold it. >>> su1 = spanner.Slur() >>> sb1 = spanner.SpannerBundle() >>> sb1.append(su1) - - It starts with no notes. - >>> su1.getSpannedElements() [] - - Now call this method to set up su1 to get the next note assigned to the spannerBundle. - This method returns a TypedDict called a `PendingAssignmentRef` which essentially - just encodes the information passed into this function: + Now call this method to register `su1` as awaiting the next note element. + The method returns a `PendingAssignmentRef` (frozen dataclass) with information + about what is registered as pending assignment. >>> sb1.setPendingSpannedElementAssignment(su1, 'Note') - {'spanner': , 'className': 'Note', - 'offsetInScore': None, 'staffKey': None} + PendingAssignmentRef(spanner=, + className='Note', offsetInScore=None) - Elements are then potentially assigned to spanners by calling - `freePendingSpannedElementAssignment` on the spannerBundle. + Elements are then potentially added to spanners by calling + `freePendingSpannedElementAssignment` on the SpannerBundle. - That method will not attach a rest like `r1` because it is not a 'Note' + That method will not attach a rest like `r1` because it is not a `'Note'`: >>> sb1.freePendingSpannedElementAssignment(r1) - >>> su1.getSpannedElements() - [] + >>> r1 in su1 + False - But `freePendingSpannedElementAssignment` will properly associate the next - Note object with the spanner. + But the next Note will be attached: >>> sb1.freePendingSpannedElementAssignment(n1) >>> su1 > - >>> su1.getSpannedElements() - [] - >>> n1.getSpannerSites() - [>] >>> n1 in su1 True + >>> n1.getSpannerSites() + [>] - And now that the assignment has been made, the pending assignment - has been cleared, so n3 will not get assigned to the slur: + Once the pending assignment has been satisfied, the registration is cleared + from the SpannerBundle, so no future notes that would have satified the assignment + get assigned: >>> sb1.freePendingSpannedElementAssignment(n3) - >>> su1.getSpannedElements() - [] >>> n3 in su1 False >>> n3.getSpannerSites() @@ -1423,43 +1415,38 @@ def setPendingSpannedElementAssignment( >>> sb1.popPendingSpannedElementAssignments() [] - Now the same deal but with offsetInScore. When offsetInScore is - specified it is used to filter out the matches - by `freePendingSpannedElementAssignment`. + Now a similar example using `offsetInScore`. When `offsetInScore` is + specified at registration, only candidates passed with a matching + offset to `freePendingSpannedElementAssignment` will be attached. Create two notes and a rest. - >>> n4 = note.Note('C') + >>> n4 = note.Note('C#') >>> wrongOffsetNote = note.Note('B') >>> r2 = note.Rest() - Create a slur with n4 already in it. + Create a slur with `n4` already in it. >>> sb2 = spanner.SpannerBundle() >>> su2 = spanner.Slur([n4]) >>> sb2.append(su2) >>> su2.getSpannedElements() - [] + [] - Now set up su2 to get the next note assigned to it at offset 4.0. + Register a pending on `su2` looking for the next Note at offset 4.0: >>> ref = sb2.setPendingSpannedElementAssignment(su2, 'Note', 4.0) - - We will also give `ref`, a `PendingAssignmentRef`, the staffKey of 1 - which could be used in later parsing (such as in MusicXML parsing to figure out - which PartStaff to assign the slur or SpannerAnchor to). It is unused by - `freePendingSpannedElementAssignment` - - >>> ref['staffKey'] = 1 >>> ref - {'spanner': >, 'className': 'Note', - 'offsetInScore': 4.0, 'staffKey': 1} + PendingAssignmentRef(spanner=>, + className='Note', offsetInScore=4.0) Call `freePendingSpannedElementAssignment` to attach an element of the right class and offset. Note that the offset must be passed to the method; it is not necessarily the offset of the object itself (most often it is the `offsetInHierarchy` of the Part object). + A note offered at the wrong offset is not attached: + >>> sb2.freePendingSpannedElementAssignment(wrongOffsetNote, 5.0) >>> wrongOffsetNote in su2 False @@ -1473,29 +1460,57 @@ def setPendingSpannedElementAssignment( We are all out of possible notes, we can see which PendingAssignmentRef elements are still awaiting elements to attach to. In this case, there is one, our - Slur + Slur: >>> unmatched_pendingAssignmentRefs = sb2.popPendingSpannedElementAssignments() >>> len(unmatched_pendingAssignmentRefs) 1 >>> unmatched_pendingAssignmentRefs[0] - {'spanner': >, 'className': 'Note', - 'offsetInScore': 4.0, 'staffKey': 1} - >>> unmatched_pendingAssignmentRefs[0]['spanner'] is su2 + PendingAssignmentRef(spanner=>, + className='Note', offsetInScore=4.0) + >>> unmatched_pendingAssignmentRefs[0].spanner is su2 True Parsers can use the information from the unmatched PendingAssignmentRef to add additional elements to the score at places where a match was expected. Given this ref, the most logical choice would be to create a `SpannerAnchor` - object and put it at score offset 4.0. The manually added `staffKey` of 1, - would help a parser remember that the SpannerAnchor should go in staff 1. - ''' - ref: PendingAssignmentRef = { - 'spanner': sp, - 'className': className, - 'offsetInScore': offsetInScore, - 'staffKey': None, - } + object and put it at score offset 4.0 and insert it as the spanner's first element + with `insertFirstSpannedElement`. + + (If a parser needs more information about where the Spanner and SpannerAnchor + should go, such as a staff number, etc. it can keep a dictionary mapping + `id(PendingAssignmentRef)` to a staffKey, etc.) + + And now the happy path: a pending assignment that finds a matching note + with the right offset. (We'll demonstrate also that you need to pass + in your own fractions.) + + >>> frac = music21.common.numberTools.opFrac + >>> n4 = note.Note('G') + >>> n5 = note.Note('A') + >>> su3 = spanner.Slur([n4]) + >>> sb3 = spanner.SpannerBundle() + >>> sb3.append(su3) + >>> _ = sb3.setPendingSpannedElementAssignment(su3, 'Note', frac(77.33333)) + >>> sb3.freePendingSpannedElementAssignment(n5, frac(77.33333)) + >>> su3 + > + >>> n5 in su3 + True + + An important detail demonstrated above a freed element is always inserted as the + first element of the spanner, even when the spanner already has + other elements. The insert-at-front behavior is important for MusicXML parsing, since + a `` is often encountered before + the note that should *start* the spanner (because of voices, other classes, etc.), + so when the starting note arrives it needs to go to the start. + + ''' + ref = PendingAssignmentRef( + spanner=sp, + className=className, + offsetInScore=offsetInScore, + ) self._pendingSpannedElementAssignment.append(ref) return ref @@ -1517,15 +1532,11 @@ def freePendingSpannedElementAssignment( remove = None for i, ref in enumerate(self._pendingSpannedElementAssignment): - # environLocal.printDebug(['calling freePendingSpannedElementAssignment()', - # self._pendingSpannedElementAssignment]) - if ref['className'] in spannedElementCandidate.classSet: + if ref.className in spannedElementCandidate.classSet: if (offsetInScore is None - or offsetInScore == ref['offsetInScore']): - ref['spanner'].insertFirstSpannedElement(spannedElementCandidate) + or offsetInScore == ref.offsetInScore): + ref.spanner.insertFirstSpannedElement(spannedElementCandidate) remove = i - # environLocal.printDebug(['freePendingSpannedElementAssignment()', - # 'added spannedElement', ref['spanner']]) break if remove is not None: self._pendingSpannedElementAssignment.pop(remove) @@ -1543,16 +1554,13 @@ def popPendingSpannedElementAssignments(self) -> list[PendingAssignmentRef]: >>> sl = spanner.Slur() >>> sb.append(sl) >>> sb.setPendingSpannedElementAssignment(sl, 'Note', 0.0) - {'spanner': , 'className': 'Note', - 'offsetInScore': 0.0, 'staffKey': None} + PendingAssignmentRef(spanner=, + className='Note', offsetInScore=0.0) - Check to make sure popPendingSpannedElementAssignments returns - the entire list, and leaves an empty list behind. - >>> expectedPending = sb._pendingSpannedElementAssignment - >>> expectedPending - [{'spanner': , 'className': 'Note', - 'offsetInScore': 0.0, 'staffKey': None}] + `popPendingSpannedElementAssignments` returns the full list and leaves + an empty list behind: + >>> expectedPending = list(sb._pendingSpannedElementAssignment) >>> pending = sb.popPendingSpannedElementAssignments() >>> pending == expectedPending True From db10ac9f3d2f78a3db67ed7ce1a658265d1ee500 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Wed, 13 May 2026 12:20:54 -1000 Subject: [PATCH 8/8] fix up text --- music21/spanner.py | 56 ++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/music21/spanner.py b/music21/spanner.py index 17f1ac21ec..297137e2b5 100644 --- a/music21/spanner.py +++ b/music21/spanner.py @@ -1334,30 +1334,18 @@ def setPendingSpannedElementAssignment( which is passed to the SpannerBundle via freePendingSpannedElementAssignment() will get it. - There are two ways to use the PendingSpannedElement APIs. One - where setPendingSpannedElementAssignment is called without specifying - offsetInScore and freePendingSpannedElementAssignment - is called without specifying a matching offset; and a more specific way where - setPendingSpannedElementAssignment is called with an offsetInScore, - freePendingSpannedElementAssignment is called - with a matching offset, and then popPendingSpannedElementAssignments is - called to get all the remaining pending assignments, so that SpannerAnchors - can be created for them (since there was no note found at the specified - offsetInScore). - - The new way is useful (for example) for importing a from - MusicXML that has specified, so that the next note parsed after - the will not be at the correct offsetInScore for the start - of the direction, and a SpannerAnchor will be required instead. - - Usage without offset: + Call `popPendingSpannedElementAssignments` after parsing to + recover pending spanners that never matched their expected elements + — e.g., a MusicXML `` whose + `` lands between notes, needing a `SpannerAnchor` instead. + + First, let's see the usage without an explicit offset: Create some notes and a rest. >>> n1 = note.Note('C') >>> r1 = note.Rest() >>> n2 = note.Note('D') - >>> n3 = note.Note('E') Notes start without any associated spanners. @@ -1400,13 +1388,13 @@ def setPendingSpannedElementAssignment( [>] Once the pending assignment has been satisfied, the registration is cleared - from the SpannerBundle, so no future notes that would have satified the assignment + from the SpannerBundle, so no future notes that would have satisfied the assignment get assigned: - >>> sb1.freePendingSpannedElementAssignment(n3) - >>> n3 in su1 + >>> sb1.freePendingSpannedElementAssignment(n2) + >>> n2 in su1 False - >>> n3.getSpannerSites() + >>> n2.getSpannerSites() [] And we can see that the SpannerBundle `sb1` has no spanners still awaiting (pending) @@ -1421,19 +1409,19 @@ def setPendingSpannedElementAssignment( Create two notes and a rest. - >>> n4 = note.Note('C#') + >>> n3 = note.Note('C#') >>> wrongOffsetNote = note.Note('B') >>> r2 = note.Rest() - Create a slur with `n4` already in it. + Create a slur with `n3` already in it. >>> sb2 = spanner.SpannerBundle() - >>> su2 = spanner.Slur([n4]) + >>> su2 = spanner.Slur([n3]) >>> sb2.append(su2) >>> su2.getSpannedElements() [] - Register a pending on `su2` looking for the next Note at offset 4.0: + Register `su2` as pending and looking for the next Note at offset 4.0: >>> ref = sb2.setPendingSpannedElementAssignment(su2, 'Note', 4.0) >>> ref @@ -1451,7 +1439,11 @@ def setPendingSpannedElementAssignment( >>> wrongOffsetNote in su2 False - Again, it will not get a rest, even at the correct offsetInScore, + (Passing an offset here is important since, when parsing MusicXML, + if a `` has `` specified, the next note might not be the one + at the start of the spanner) + + The freePending method will not get a rest even at the correct offsetInScore because it is not the class being searched for. >>> sb2.freePendingSpannedElementAssignment(r2, 4.0) @@ -1485,7 +1477,7 @@ def setPendingSpannedElementAssignment( with the right offset. (We'll demonstrate also that you need to pass in your own fractions.) - >>> frac = music21.common.numberTools.opFrac + >>> frac = common.numberTools.opFrac >>> n4 = note.Note('G') >>> n5 = note.Note('A') >>> su3 = spanner.Slur([n4]) @@ -1498,13 +1490,13 @@ def setPendingSpannedElementAssignment( >>> n5 in su3 True - An important detail demonstrated above a freed element is always inserted as the + An important detail demonstrated above is that a freed element is always inserted as the first element of the spanner, even when the spanner already has - other elements. The insert-at-front behavior is important for MusicXML parsing, since - a `` is often encountered before + other elements. (Note A is before note G in the slur). + The insert-at-front behavior is important for MusicXML parsing, since + a `` specified is often encountered before the note that should *start* the spanner (because of voices, other classes, etc.), so when the starting note arrives it needs to go to the start. - ''' ref = PendingAssignmentRef( spanner=sp,