Repository: cuthbertLab/music21

Issue:
Tuplet setter doesn't inform client
Decided to document what I learned about the necessity of calling `coreElementsChanged()` when setting `.tuplets` directly.

Should spruce that up someday--there's not a good workaround if you're trying to *remove* tuplets, since `appendTuplet` only appends, and it breaks our principle that only core methods should require calling `coreElementsChanged()`.

_Originally posted by @jacobtylerwalls in https://github.com/cuthbertLab/music21/issues/958#issuecomment-818046530_

***

I see a few approaches:

- tuplet setter could inform the client instead of `appendTuplet` having to do it (I tried this and it broke a test, but the test may have been measuring the wrong thing, so it may unwravel some shoelaces that need to be unwraveled anyway)
- new method `removeTuplet` to address the gap in functionality
- clear tuplets in `Duration.clear()`

Not going to jump on this, doesn't seem urgent for v7.

Comments:

Patch:
diff --git a/music21/duration.py b/music21/duration.py
index 6155728c1..051d1f7a3 100644
--- a/music21/duration.py
+++ b/music21/duration.py
@@ -1821,8 +1821,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
         True
         '''
         newTuplet.frozen = True
-        self.tuplets = self.tuplets + (newTuplet,)
-        self.informClient()
+        self.tuplets = self._tuplets + (newTuplet,)
 
     def augmentOrDiminish(self, amountToScale, retainComponents=False):
         '''
@@ -2846,9 +2845,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
     def tuplets(self) -> Tuple[Tuplet, ...]:
         '''
         Return a tuple of Tuplet objects.
-        May leave a stream containing objects having this duration
-        in an unusable state, requiring :meth:`~music21.stream.core.coreElementsChanged`
-        to be called. For this reason, prefer using :meth:`appendTuplet` to add tuplets.
+        Setting tuplets will inform the client (Note) that the duration has changed.
         '''
         if self._componentsNeedUpdating:
             self._updateComponents()
@@ -2859,6 +2856,7 @@ class Duration(prebase.ProtoM21Object, SlottedObjectMixin):
         # environLocal.printDebug(['assigning tuplets in Duration', tupletTuple])
         self._tuplets = tuple(tupletTuple)
         self._quarterLengthNeedsUpdating = True
+        self.informClient()
 
     def aggregateTupletMultiplier(self) -> OffsetQL:
         '''
@@ -3593,7 +3591,7 @@ class Test(unittest.TestCase):
         self.assertEqual(str(d.components),
                          "(DurationTuple(type='eighth', dots=0, quarterLength=0.5),)")
         self.assertFalse(d._componentsNeedUpdating)
-        self.assertTrue(d._quarterLengthNeedsUpdating)
+        self.assertFalse(d._quarterLengthNeedsUpdating)
         self.assertEqual(repr(d.quarterLength), 'Fraction(1, 3)')
         self.assertEqual(str(unitSpec(d)), "(Fraction(1, 3), 'eighth', 0, 3, 2, 'eighth')")
 
diff --git a/music21/harmony.py b/music21/harmony.py
index 20345c1bc..ae2f5ad6a 100644
--- a/music21/harmony.py
+++ b/music21/harmony.py
@@ -1387,8 +1387,8 @@ class ChordSymbol(Harmony):
     chords, by default appear as chord symbols in a score and have duration of
     0.
 
-    To obtain the chord representation of the in the score, change the
-    :attr:`music21.harmony.ChordSymbol.writeAsChord` to True. Unless otherwise
+    To obtain the chord representation of the `ChordSymbol` in the score, change
+    :attr:`~music21.harmony.Harmony.writeAsChord` to True. Unless otherwise
     specified, the duration of this chord object will become 1.0. If you have a
     leadsheet, run :meth:`music21.harmony.realizeChordSymbolDurations` on the
     stream to assign the correct (according to offsets) duration to each
diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py
index 781f39705..e786eaf6b 100644
--- a/music21/musicxml/xmlToM21.py
+++ b/music21/musicxml/xmlToM21.py
@@ -1984,6 +1984,17 @@ class PartParser(XMLParserBase):
 
         sets self.lastMeasureWasShort to True or False if it is an incomplete measure
         that is not a pickup.
+
+        >>> m = stream.Measure([meter.TimeSignature('4/4'), harmony.ChordSymbol('C7')])
+        >>> m.highestTime
+        0.0
+        >>> PP = musicxml.xmlToM21.PartParser()
+        >>> PP.setLastMeasureInfo(m)
+        >>> PP.adjustTimeAttributesFromMeasure(m)
+        >>> m.highestTime
+        4.0
+        >>> PP.lastMeasureWasShort
+        False
         '''
         # note: we cannot assume that the time signature properly
         # describes the offsets w/n this bar. need to look at
@@ -1999,7 +2010,9 @@ class PartParser(XMLParserBase):
         if mHighestTime >= lastTimeSignatureQuarterLength:
             mOffsetShift = mHighestTime
 
-        elif mHighestTime == 0.0 and not m.recurse().notesAndRests:
+        elif (mHighestTime == 0.0
+              and not m.recurse().notesAndRests.getElementsNotOfClass('Harmony')
+              ):
             # this routine fixes a bug in PDFtoMusic and other MusicXML writers
             # that omit empty rests in a Measure.  It is a very quick test if
             # the measure has any notes.  Slower if it does not.
@@ -2007,6 +2020,7 @@ class PartParser(XMLParserBase):
             r.duration.quarterLength = lastTimeSignatureQuarterLength
             m.insert(0.0, r)
             mOffsetShift = lastTimeSignatureQuarterLength
+            self.lastMeasureWasShort = False
 
         else:  # use time signature
             # for the first measure, this may be a pickup


Test patch:
diff --git a/music21/stream/tests.py b/music21/stream/tests.py
index 3d1165e16..0d680c827 100644
--- a/music21/stream/tests.py
+++ b/music21/stream/tests.py
@@ -553,14 +553,15 @@ class Test(unittest.TestCase):
         a.append(n)
         self.assertEqual(a.duration.quarterLength, 1.0)
 
-        # Alter the note's duration in a nonstandard way
-        # Normally, one would call Duration.appendTuplet()
         tup = duration.Tuplet()
         n.duration.tuplets = (tup,)
-        # Nonstandard workflow requires coreElementsChanged() to be called
+        self.assertEqual(a.duration.quarterLength, Fraction(2, 3))
+
+        # Also (regression) test clearing the cache
         # https://github.com/cuthbertLab/music21/issues/957
+        n.duration.tuplets = (tup, tup)
         a.coreElementsChanged()
-        self.assertEqual(a.duration.quarterLength, Fraction(2, 3))
+        self.assertEqual(a.duration.quarterLength, Fraction(4, 9))
 
     def testMeasureStream(self):
         '''An approach to setting TimeSignature measures in offsets and durations