diff --git a/music21/common/objects.py b/music21/common/objects.py index f6993e92fb..43fc7dc169 100644 --- a/music21/common/objects.py +++ b/music21/common/objects.py @@ -295,11 +295,13 @@ def _check_init(self, key: str|None = None) -> bool: if not getattr(self, 'frozen', True): return True - for st in inspect.stack(): - if (st.frame.f_code.co_name in ('__init__', '__new__', '__setstate__') - and 'self' in st.frame.f_locals - and st.frame.f_locals['self'].__class__ == self.__class__): + frame = inspect.currentframe() + while frame is not None: + if (frame.f_code.co_name in ('__init__', '__new__', '__setstate__') + and 'self' in frame.f_locals + and frame.f_locals['self'].__class__ == self.__class__): return True + frame = frame.f_back raise TypeError(f'This {self.__class__.__name__} instance is immutable.') def __setattr__(self, key: str, value: t.Any) -> None: diff --git a/music21/meter/base.py b/music21/meter/base.py index 673e750b87..ab2ed1edcf 100644 --- a/music21/meter/base.py +++ b/music21/meter/base.py @@ -35,7 +35,7 @@ from music21.meter.tools import slashToTuple, proportionToFraction from music21.meter.core import ( - MeterSequence, getMeterSequence, getMeterTerminal, makeSequence, + MeterSequence, getMeterSequence, getMeterTerminal, ) environLocal = environment.Environment('meter') @@ -1176,11 +1176,7 @@ def summedNumerator(self) -> bool: @summedNumerator.setter def summedNumerator(self, value: bool): if self.displaySequence is not None: - # displaySequence is immutable; swap in a copy with the new flag - ds = self.displaySequence - self.displaySequence = makeSequence( - ds.numerator, ds.denominator, ds._partition, - parenthesis=ds.parenthesis, summedNumerator=value) + self.displaySequence = self.displaySequence.modify(summedNumerator=value) # -------------------------------------------------------------------------- # private methods -- most to be put into the various sequences. @@ -1341,14 +1337,13 @@ def _setDefaultAccentWeights(self, depth: int = 3) -> None: weightValues[x + 1] = weightValueMin * pow(2, x) # set weights on accent partitions - self.accentSequence = self.accentSequence.partition([1] * accentCount) - for i in range(accentCount): - # get values from weightValues dictionary; terminals are - # immutable, so replace each with a re-weighted cached one - mt = self.accentSequence[i] - self.accentSequence = self.accentSequence.setIndex( - i, getMeterTerminal( - mt.numerator, mt.denominator, weight=weightValues[weightInts[i]])) + accentSequence = self.accentSequence.partition([1] * accentCount) + weighted = [] + for i, mt in enumerate(accentSequence): + # get values from weightValues dictionary + weighted.append(getMeterTerminal( + mt.numerator, mt.denominator, weight=weightValues[weightInts[i]])) + self.accentSequence = accentSequence.modify(partition=weighted) # -------------------------------------------------------------------------- # access data for other processing diff --git a/music21/meter/core.py b/music21/meter/core.py index df9bc39d2b..64b189488f 100644 --- a/music21/meter/core.py +++ b/music21/meter/core.py @@ -118,9 +118,6 @@ def duration(self) -> FrozenDuration: ''' return self._durationFromNumeratorDenominator(self.numerator, self.denominator) - def _reprInternal(self): - return str(self) - def ratioEqual(self, other) -> bool: ''' Compare the numerator and denominator of another object. @@ -2253,19 +2250,26 @@ def _getAtPath(root: MeterSequence, path: tuple[int, ...]) -> MeterNode: return node -def _replaceAtPath(root: MeterSequence, path: tuple[int, ...], newNode) -> MeterSequence: +def _replaceAtPaths( + node: MeterNode, + replacements: dict[tuple[int, ...], MeterNode], + ancestors: set[tuple[int, ...]], + path: tuple[int, ...] = (), +) -> MeterNode: ''' - Return a new root MeterSequence with the node at `path` replaced by - `newNode`, rebuilding each ancestor functionally. + Return `node` with each node at a path in `replacements` replaced, rebuilding + only the sequences in `ancestors`, the paths above a replacement. AI-assisted (Claude). ''' - idx = path[0] - if len(path) == 1: - return root.setIndex(idx, newNode) - child = t.cast('MeterSequence', root[idx]) - newChild = _replaceAtPath(child, path[1:], newNode) - return root.setIndex(idx, newChild) + if path in replacements: + return replacements[path] + if path not in ancestors: + return node + sequence = t.cast('MeterSequence', node) + newPartition = [_replaceAtPaths(child, replacements, ancestors, path + (i,)) + for i, child in enumerate(sequence)] + return sequence._rebuild(newPartition) def _subdivideNestedByPaths( @@ -2274,20 +2278,22 @@ def _subdivideNestedByPaths( divisions, ) -> tuple[MeterSequence, list[tuple[int, ...]]]: ''' - Subdivide (equally) each node reached by a path in `paths`, rebuilding the - root functionally, and return the new root plus the list of paths to the - newly created child nodes. + Subdivide each node at `paths` equally. No path may lie inside another. + Return the new root and the paths to the new child nodes. AI-assisted (Claude). ''' + replacements: dict[tuple[int, ...], MeterNode] = {} newPaths: list[tuple[int, ...]] = [] for p in paths: node = t.cast('MeterSequence', _getAtPath(root, p)) newNode = node.subdividePartitionsEqual(divisions) - root = _replaceAtPath(root, p, newNode) + replacements[p] = newNode for j in range(len(newNode)): newPaths.append(p + (j,)) - return root, newPaths + ancestors = {p[:k] for p in paths for k in range(len(p))} + newRoot = t.cast('MeterSequence', _replaceAtPaths(root, replacements, ancestors)) + return newRoot, newPaths if __name__ == '__main__':