From fc440f0e3922ec575744ab94fc3e34afba4f0620 Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Sat, 22 Aug 2026 13:45:06 +0530 Subject: [PATCH 1/3] Add Stream.isAtEnd() --- music21/stream/base.py | 23 +++++++++++++++++++++++ music21/stream/tests.py | 26 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/music21/stream/base.py b/music21/stream/base.py index 121bcc0bfd..77bb32c3ce 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -2699,6 +2699,29 @@ def storeAtEnd(self, itemOrList, ignoreSort=False): # Streams cannot reside in end elements, thus do not update is flat self.coreElementsChanged(updateIsFlat=False) + def isAtEnd(self, element: base.Music21Object) -> bool: + ''' + Return True if `element` is stored at the end of this Stream + (via :meth:`~music21.stream.base.Stream.storeAtEnd`). + + >>> s = stream.Stream() + >>> n = note.Note('C') + >>> s.append(n) + >>> b = bar.Barline() + >>> s.storeAtEnd(b) + >>> s.isAtEnd(b) + True + >>> s.isAtEnd(n) + False + + The element must already be in this Stream. + + * New in v11. + + AI-assisted. + ''' + return self.elementOffset(element, returnSpecial=True) is OffsetSpecial.AT_END + # -------------------------------------------------------------------------- # all the following call either insert() or append() diff --git a/music21/stream/tests.py b/music21/stream/tests.py index 5f28356852..52e06d0916 100644 --- a/music21/stream/tests.py +++ b/music21/stream/tests.py @@ -5063,6 +5063,32 @@ def testStoreAtEndFailures(self): with self.assertRaises(StreamException): s.storeAtEnd(b2) + def testIsAtEnd(self): + ''' + https://github.com/cuthbertLab/music21/issues/1069 + + AI-assisted. + ''' + s = Stream() + n = note.Note('C') + s.append(n) + b = bar.Barline() + s.storeAtEnd(b) + self.assertTrue(s.isAtEnd(b)) + self.assertFalse(s.isAtEnd(n)) + + m = Measure() + nM = note.Note('D') + m.append(nM) + rb = bar.Barline('final') + m.storeAtEnd(rb) + self.assertTrue(m.isAtEnd(rb)) + self.assertFalse(m.isAtEnd(nM)) + + outsider = note.Note('E') + with self.assertRaises(sites.SitesException): + s.isAtEnd(outsider) + def testElementsHighestTimeB(self): ''' Test adding elements at the highest time position From dba4ee5cf6b2f9853b546233136bb8e8914b667a Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Sun, 23 Aug 2026 10:28:51 +0530 Subject: [PATCH 2/3] docs: show SitesException when isAtEnd() is called on a missing element --- music21/stream/base.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/music21/stream/base.py b/music21/stream/base.py index 77bb32c3ce..0fa99662bf 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -2714,7 +2714,13 @@ def isAtEnd(self, element: base.Music21Object) -> bool: >>> s.isAtEnd(n) False - The element must already be in this Stream. + If the element is not in this Stream, :exc:`~music21.sites.SitesException` + is raised: + + >>> s.isAtEnd(note.Note('E')) + Traceback (most recent call last): + music21.sites.SitesException: an entry for this object 0x... is not stored in stream + * New in v11. From c9bd0d07c94e204dcfd816ae3a915903b401dee1 Mon Sep 17 00:00:00 2001 From: Sankalp Thakur Date: Mon, 24 Aug 2026 06:58:14 +0530 Subject: [PATCH 3/3] Use isAtEnd() in quantize() to skip elements stored at the end quantize() iterated the private _elements to avoid touching elements stored at the end. Iterate the public .elements instead and skip them with the new isAtEnd(), which is the use case the method was added for. This is not only cosmetic: elementOffset() returns a concrete offset for an at-end element, so quantizing one would overwrite its AT_END marker while leaving it in _endElements. The added test fails without the skip. --- music21/stream/base.py | 5 ++++- music21/stream/tests.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/music21/stream/base.py b/music21/stream/base.py index 0fa99662bf..a8f2a9bcf3 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -9529,7 +9529,10 @@ def findNextElementNotCoincident( # is unsorted originally, this "looking ahead" could become O(n^2). originallySorted = useStream.isSorted rests_lacking_durations: list[note.Rest] = [] - for i, e in enumerate(useStream._elements): + for i, e in enumerate(useStream.elements): + if useStream.isAtEnd(e): + # Elements stored at the end carry no quantizable offset. + continue if processOffsets: o = useStream.elementOffset(e) sign = 1 diff --git a/music21/stream/tests.py b/music21/stream/tests.py index 52e06d0916..0686170f98 100644 --- a/music21/stream/tests.py +++ b/music21/stream/tests.py @@ -5089,6 +5089,26 @@ def testIsAtEnd(self): with self.assertRaises(sites.SitesException): s.isAtEnd(outsider) + def testQuantizeKeepsElementsStoredAtEnd(self): + ''' + quantize() iterates .elements and skips elements stored at the end, so a + barline keeps its AT_END position instead of being given a real offset. + + AI-assisted. + ''' + s = Stream() + n = note.Note() + n.quarterLength = 0.26 + s.repeatInsert(n, [0.1, 0.49, 0.9]) + b = bar.Barline() + s.storeAtEnd(b) + + q = s.quantize(processOffsets=True, processDurations=True, inPlace=False) + + quantizedBarline = q._endElements[0] + self.assertTrue(q.isAtEnd(quantizedBarline)) + self.assertEqual([q.elementOffset(e) for e in q._elements], [0.0, 0.5, 1.0]) + def testElementsHighestTimeB(self): ''' Test adding elements at the highest time position