diff --git a/documentation/source/usersGuide/usersGuide_55_advancedMeter.ipynb b/documentation/source/usersGuide/usersGuide_55_advancedMeter.ipynb index b256b237ad..4a777cd2d8 100644 --- a/documentation/source/usersGuide/usersGuide_55_advancedMeter.ipynb +++ b/documentation/source/usersGuide/usersGuide_55_advancedMeter.ipynb @@ -181,17 +181,17 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A MeterSequence object is a sub-class of a MeterTerminal. Like a MeterTerminal, a MeterSequence has a :attr:`numerator `, a :attr:`denominator `, and a :attr:`duration ` attribute. A MeterSequence, however, can be a hierarchical tree or sub-tree, containing an ordered sequence of MeterTerminal and/or MeterSequence objects.\n", + "Like a MeterTerminal, a MeterSequence has a :attr:`numerator `, a :attr:`denominator `, and a :attr:`duration ` attribute. A MeterSequence, however, can be a hierarchical tree or sub-tree, containing an ordered sequence of MeterTerminal and/or MeterSequence objects.\n", "\n", "The ordered collection of MeterTerminal and/or MeterSequence objects can be accessed like Python lists. MeterSequence objects, like MeterTerminal objects, store a weight that by default is the sum of constituent weights.\n", "\n", - "The :meth:`~music21.meter.MeterSequence.partition` and :meth:`~music21.meter.MeterTerminal.subdivide` methods can be used to configure the nested hierarchical structure.\n", + "MeterTerminal and MeterSequence objects cannot be changed. The :meth:`~music21.meter.MeterSequence.partition`, :meth:`~music21.meter.MeterTerminal.subdivide`, and :meth:`~music21.meter.MeterSequence.setIndex` methods each return a new object, which can be assigned to a variable or to a TimeSignature attribute, to configure the nested hierarchical structure.\n", "\n", - "The :meth:`~music21.meter.MeterSequence.partition` method replaces existing MeterTerminal or MeterSequence objects in place with a new arrangement, specified as a desired number of equal-valued components, a list of numerators assuming equal-denominators, or a list of string fraction representations.\n", + "The :meth:`~music21.meter.MeterSequence.partition` method returns a new MeterSequence with a new arrangement, specified as a desired number of equal-valued components, a list of numerators assuming equal-denominators, or a list of string fraction representations.\n", "\n", "The :meth:`~music21.meter.MeterTerminal.subdivide` method returns a new MeterSequence (leaving the source MeterSequence unchanged) with an arrangement of MeterTerminals as specified by an argument in the same form as for the :meth:`~music21.meter.MeterSequence.partition` method.\n", "\n", - "Note that MeterTerminal objects cannot be partitioned in place. A common way to convert a MeterTerminal into a MeterSequence is to reassign the returned MeterSequence from the :meth:`~music21.meter.MeterTerminal.subdivide` method to the position occupied by the MeterTerminal.\n", + "The :meth:`~music21.meter.MeterSequence.setIndex` method returns a new MeterSequence with one position replaced by an object of the same length. A common way to convert a MeterTerminal into a MeterSequence is to pass the MeterSequence returned from the :meth:`~music21.meter.MeterTerminal.subdivide` method to :meth:`~music21.meter.MeterSequence.setIndex` for the position occupied by the MeterTerminal.\n", "\n", "The following example creates and partitions a MeterSequence by re-assigning subdivisions to MeterTerminal objects. The use of Python list-like index access is also demonstrated.\n" ] @@ -234,7 +234,7 @@ } ], "source": [ - "ms.partition([3,3])\n", + "ms = ms.partition([3, 3])\n", "ms" ] }, @@ -275,7 +275,7 @@ } ], "source": [ - "ms[0] = ms[0].subdivide([3,3])\n", + "ms = ms.setIndex(0, ms[0].subdivide([3, 3]))\n", "ms[0]" ] }, @@ -316,7 +316,7 @@ } ], "source": [ - "ms[1] = ms[1].subdivide([1,1,1])\n", + "ms = ms.setIndex(1, ms[1].subdivide([1, 1, 1]))\n", "ms[1][0]" ] }, @@ -748,9 +748,9 @@ "source": [ "from music21 import stream, note\n", "ts1 = meter.TimeSignature('5/8') # assumes two partitions\n", - "ts1.displaySequence.partition(['3/16', '1/8', '5/16'])\n", + "ts1.displaySequence = ts1.displaySequence.partition(['3/16', '1/8', '5/16'])\n", "ts2 = meter.TimeSignature('5/8') # assumes two partitions\n", - "ts2.displaySequence.partition(['2/8', '3/8'])\n", + "ts2.displaySequence = ts2.displaySequence.partition(['2/8', '3/8'])\n", "ts2.summedNumerator = True\n", "s = stream.Stream()\n", "for ts in [ts1, ts2]:\n", @@ -807,24 +807,26 @@ ], "source": [ "ts1 = meter.TimeSignature('3/4')\n", - "ts1.beamSequence.partition(1)\n", - "ts1.beamSequence[0] = ts1.beamSequence[0].subdivide(['3/8', '5/32', '4/32', '3/32'])\n", + "ts1.beamSequence = ts1.beamSequence.partition(1)\n", + "ts1.beamSequence = ts1.beamSequence.setIndex(\n", + " 0, ts1.beamSequence[0].subdivide(['3/8', '5/32', '4/32', '3/32']))\n", "\n", "ts2 = meter.TimeSignature('3/4')\n", - "ts2.beamSequence.partition(3)\n", + "ts2.beamSequence = ts2.beamSequence.partition(3)\n", "\n", "ts3 = meter.TimeSignature('3/4')\n", - "ts3.beamSequence.partition(3)\n", - "\n", + "ts3.beamSequence = ts3.beamSequence.partition(3)\n", "for i in range(len(ts3.beamSequence)):\n", - " ts3.beamSequence[i] = ts3.beamSequence[i].subdivide(2)\n", + " ts3.beamSequence = ts3.beamSequence.setIndex(i, ts3.beamSequence[i].subdivide(2))\n", "\n", "ts4 = meter.TimeSignature('3/4')\n", - "ts4.beamSequence.partition(['3/8', '3/8'])\n", - "for i in range(len(ts4.beamSequence)):\n", - " ts4.beamSequence[i] = ts4.beamSequence[i].subdivide(['6/32', '6/32'])\n", - " for j in range(len(ts4.beamSequence[i])):\n", - " ts4.beamSequence[i][j] = ts4.beamSequence[i][j].subdivide(2)\n", + "beams = ts4.beamSequence.partition(['3/8', '3/8'])\n", + "for i in range(len(beams)):\n", + " group = beams[i].subdivide(['6/32', '6/32'])\n", + " for j in range(len(group)):\n", + " group = group.setIndex(j, group[j].subdivide(2))\n", + " beams = beams.setIndex(i, group)\n", + "ts4.beamSequence = beams\n", "\n", "s = stream.Stream()\n", "for ts in [ts1, ts2, ts3, ts4]:\n", @@ -932,7 +934,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 35, @@ -1093,7 +1095,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 43, @@ -1245,7 +1247,7 @@ "source": [ "score = corpus.parse('bach/bwv366.xml')\n", "ts = score[meter.TimeSignature].first()\n", - "ts.beatSequence.partition(3)\n", + "ts.beatSequence = ts.beatSequence.partition(3)\n", "\n", "found = stream.Stream()\n", "offsetQL = 0\n", @@ -1278,7 +1280,7 @@ "\n", "The :meth:`~music21.meter.TimeSignature.getBeatDepth` method, when set with the optional parameter `align` to \"quantize\", shows the number of hierarchical levels that start at or before that point. This value is described by Lerdahl and Jackendoff as metrical analysis.\n", "\n", - "In the following example, :meth:`beatSequence ` MeterSequence is partitioned first into one subdivision, and then each subsequent subdivision into two, down to four layers of partitioning.\n", + "In the following example, :meth:`beatSequence ` MeterSequence is partitioned first into one subdivision, and then each subsequent subdivision into two, down to four layers of partitioning, with the :meth:`~music21.meter.MeterSequence.subdivideNestedHierarchy` method.\n", "\n", "The number of hierarchical levels, found with the :meth:`~music21.meter.TimeSignature.getBeatDepth` method, is appended to each note with the :meth:`~music21.note.GeneralNote.addLyric` method." ] @@ -1308,13 +1310,7 @@ "score = corpus.parse('bach/bwv281.xml')\n", "partBass = score.getElementById('Bass')\n", "ts = partBass[meter.TimeSignature].first()\n", - "ts.beatSequence.partition(1)\n", - "for h in range(len(ts.beatSequence)):\n", - " ts.beatSequence[h] = ts.beatSequence[h].subdivide(2)\n", - " for i in range(len(ts.beatSequence[h])):\n", - " ts.beatSequence[h][i] = ts.beatSequence[h][i].subdivide(2)\n", - " for j in range(len(ts.beatSequence[h][i])):\n", - " ts.beatSequence[h][i][j] = ts.beatSequence[h][i][j].subdivide(2)\n", + "ts.beatSequence = ts.beatSequence.subdivideNestedHierarchy(3)\n", "\n", "for m in partBass.getElementsByClass(stream.Measure):\n", " for n in m.notesAndRests:\n", @@ -1388,8 +1384,8 @@ "score = corpus.parse('bach/bwv366.xml')\n", "partBass = score.getElementById('Bass')\n", "ts = partBass[meter.TimeSignature].first()\n", - "ts.beatSequence.partition(['3/8', '3/8'])\n", - "ts.accentSequence.partition(['3/8', '3/8'])\n", + "ts.beatSequence = ts.beatSequence.partition(['3/8', '3/8'])\n", + "ts.accentSequence = ts.accentSequence.partition(['3/8', '3/8'])\n", "ts.setAccentWeight([1, .5])\n", "for m in partBass[stream.Measure]:\n", " lastBeat = None\n", diff --git a/music21/meter/core.py b/music21/meter/core.py index df9bc39d2b..37b0ae123b 100644 --- a/music21/meter/core.py +++ b/music21/meter/core.py @@ -525,10 +525,10 @@ class MeterSequence(prebase.ProtoM21Object, MeterCore, FrozenObject): * Changed in v11: MeterSequence is immutable, hashable, deep-copies to itself, and its formerly in-place operations return a new sequence. - Removed `load()` (set values when creating the sequence), - `subdivideByOther()` (it only wrapped its argument in a new sequence), - the `weight` setter (use `withWeight()`), and item assignment - `ms[i] = x` (use `setIndex()`). + `load()` raises a MeterException (set values when creating the + sequence) and item assignment `ms[i] = x` raises a TypeError (use + `setIndex()`). Removed `subdivideByOther()` (it only wrapped its + argument in a new sequence) and the `weight` setter (use `withWeight()`). AI-assisted (Claude). ''' @@ -691,6 +691,36 @@ def __len__(self): ''' return len(self._partition) + def __setitem__(self, key: int, value: MeterNode) -> t.NoReturn: + ''' + Raises a TypeError: a MeterSequence is immutable. Use :meth:`setIndex`. + + >>> a = meter.MeterSequence('4/4', 4) + >>> a[0] = a[0].subdivide(2) + Traceback (most recent call last): + TypeError: MeterSequence is immutable: use ms = ms.setIndex(0, value) + + * Changed in v11: raises a TypeError. + ''' + raise TypeError(f'MeterSequence is immutable: use ms = ms.setIndex({key}, value)') + + def load(self, *arguments, **keywords) -> t.NoReturn: + ''' + Raises a MeterException: a MeterSequence is immutable. Pass the value + to the constructor or to :func:`getMeterSequence` instead. + + >>> ms = meter.MeterSequence() + >>> ms.load('4/4') + Traceback (most recent call last): + music21.exceptions21.MeterException: MeterSequence is immutable, so load() + cannot change it: use ms = meter.MeterSequence('4/4') + + * Changed in v11: no longer changes the sequence in place. + ''' + value = repr(arguments[0]) if arguments else '...' + raise MeterException('MeterSequence is immutable, so load() cannot change it: ' + f'use ms = meter.MeterSequence({value})') + def setIndex(self, index: int, value: MeterNode) -> MeterSequence: ''' Return a new MeterSequence with the element at `index` replaced by diff --git a/music21/meter/tests.py b/music21/meter/tests.py index 8f7999c955..50c7e9fc6e 100644 --- a/music21/meter/tests.py +++ b/music21/meter/tests.py @@ -177,6 +177,27 @@ def testMeterSequenceModify(self): with self.assertRaises(ValueError): a.modify(**kw) + def testInPlaceFormsRaise(self): + from music21.exceptions21 import MeterException + ts = TimeSignature('6/8') + before = ts.beatSequence + with self.assertRaisesRegex(MeterException, r"MeterSequence\('4/4'\)"): + MeterSequence().load('4/4') + with self.assertRaisesRegex(TypeError, r'setIndex\(0, value\)'): + ts.beatSequence[0] = ts.beatSequence[0].subdivide(3) + + mt = MeterTerminal('3/4') + for attribute, value in (('numerator', 2), ('denominator', 8), ('weight', 0.5), + ('duration', duration.Duration(2.0))): + with self.assertRaises(TypeError, msg=attribute): + setattr(mt, attribute, value) + for attribute, value in (('weight', 0.5), ('parenthesis', True), + ('summedNumerator', True)): + with self.assertRaises(TypeError, msg=attribute): + setattr(ts.beatSequence, attribute, value) + self.assertIs(ts.beatSequence, before) + self.assertEqual(str(mt), '3/4') + def testGetBeams(self): ts = TimeSignature('6/8') durList = [16, 16, 16, 16, 8, 16, 16, 16, 16, 8]