diff --git a/dist/dist.py b/dist/dist.py index 4adb9f9cbe..b60784a3d1 100644 --- a/dist/dist.py +++ b/dist/dist.py @@ -17,8 +17,8 @@ 1. update the VERSION in _version.py and the single test cases in base.py. 2. run `corpus.corpora.CoreCorpus().cacheMetadata()`. for a major change run corpus.corpora.CoreCorpus().rebuildMetadataCache() - (40 min on MacPro) -- either of these MAY change a lot of tests in corpus, metadata, etc. - so don't skip the next step! + (20 min on IntelMacbook Air) -- either of these MAY change a + lot of tests in corpus, metadata, etc. so don't skip the next step! 3. run test/warningMultiprocessTest.py for lowest and highest Py version -- fix all warnings! 4. run test/testLint.py and fix any lint errors (covered now by CI) 5. commit and then check test/testSingleCoreAll.py or wait for results on GitHub Actions diff --git a/music21/_version.py b/music21/_version.py index 3f1f70550d..460e8eae5d 100644 --- a/music21/_version.py +++ b/music21/_version.py @@ -47,7 +47,7 @@ ''' from __future__ import annotations -__version__ = '9.0.0a4' +__version__ = '9.0.0a5' def get_version_tuple(vv): diff --git a/music21/abcFormat/__init__.py b/music21/abcFormat/__init__.py index ff0abe1a10..7f2a822899 100644 --- a/music21/abcFormat/__init__.py +++ b/music21/abcFormat/__init__.py @@ -3439,6 +3439,9 @@ def readstr(self, strSrc: str, number: int | None = None) -> ABCHandler: # ------------------------------------------------------------------------------ class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) def testTokenization(self): from music21.abcFormat import testFiles diff --git a/music21/alpha/analysis/fixer.py b/music21/alpha/analysis/fixer.py index d336ae9174..07ec080985 100644 --- a/music21/alpha/analysis/fixer.py +++ b/music21/alpha/analysis/fixer.py @@ -504,6 +504,10 @@ def __init__(self, changes, midiStream, omrStream): super().__init__(changes, midiStream, omrStream, recognizer) class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) + def measuresEqual(self, m1, m2): ''' Returns a tuple of (True/False, reason) diff --git a/music21/alpha/analysis/hasher.py b/music21/alpha/analysis/hasher.py index de70294c59..0c7c9d2e3b 100644 --- a/music21/alpha/analysis/hasher.py +++ b/music21/alpha/analysis/hasher.py @@ -585,6 +585,9 @@ def __new__(cls, tupEls): class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) def _approximatelyEqual(self, a, b, sig_fig=2): ''' diff --git a/music21/alpha/analysis/ornamentRecognizer.py b/music21/alpha/analysis/ornamentRecognizer.py index 82ab0e6b8f..99a680f0b0 100644 --- a/music21/alpha/analysis/ornamentRecognizer.py +++ b/music21/alpha/analysis/ornamentRecognizer.py @@ -262,6 +262,10 @@ def __init__( self.isInverted = isInverted class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) + def testRecognizeTurn(self): # set up experiment testConditions = [] diff --git a/music21/alpha/analysis/search.py b/music21/alpha/analysis/search.py index de652f297f..4e0c64d163 100644 --- a/music21/alpha/analysis/search.py +++ b/music21/alpha/analysis/search.py @@ -251,6 +251,9 @@ def findConsecutiveScale(source, targetScale, degreesRequired=5, # ------------------------------------------------------------------------------ class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) def testFindConsecutiveScaleA(self): sc = scale.MajorScale('a4') diff --git a/music21/analysis/discrete.py b/music21/analysis/discrete.py index ccc4e5225c..3023abcd9f 100644 --- a/music21/analysis/discrete.py +++ b/music21/analysis/discrete.py @@ -1413,6 +1413,9 @@ def analysisClassFromMethodName(method: str) -> type[DiscreteAnalysis] | None: class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) def testKeyAnalysisKrumhansl(self): from music21 import converter diff --git a/music21/analysis/reduction.py b/music21/analysis/reduction.py index 78e5038257..e1bdab51f2 100644 --- a/music21/analysis/reduction.py +++ b/music21/analysis/reduction.py @@ -891,6 +891,9 @@ def getGraphHorizontalBarWeightedData(self): # ------------------------------------------------------------------------------ class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) def testExtractionA(self): from music21 import analysis diff --git a/music21/analysis/transposition.py b/music21/analysis/transposition.py index 8bfecc6c40..edcd46ff27 100644 --- a/music21/analysis/transposition.py +++ b/music21/analysis/transposition.py @@ -16,16 +16,11 @@ from music21 import chord from music21 import common from music21 import environment -from music21 import exceptions21 from music21 import pitch environLocal = environment.Environment('analysis.transposition') -class TranspositionException(exceptions21.Music21Exception): - pass - - class TranspositionChecker: ''' Given a list of pitches, checks for the number of distinct transpositions. @@ -55,14 +50,14 @@ class TranspositionChecker: ''' def __init__(self, pitches: Iterable[pitch.Pitch] = ()): if not pitches: - raise TranspositionException( + raise TypeError( 'Must have at least one element in list' ) if not common.isIterable(pitches): - raise TranspositionException('Must be a list or tuple') + raise TypeError('Must be a list or tuple') # p0 = pitches[0] # if not isinstance(p0, pitch.Pitch): - # raise TranspositionException('List must have pitch objects') + # raise TypeError('List must have pitch objects') self.pitches: Iterable[pitch.Pitch] = pitches self.allTranspositions: list = [] self.allNormalOrders: list = [] @@ -191,6 +186,9 @@ def getPitchesOfDistinctTranspositions(self): # ------------------------------------------------------------------------------ class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) def testConstructTranspositionChecker(self): p = [pitch.Pitch('D#')] diff --git a/music21/analysis/windowed.py b/music21/analysis/windowed.py index 904e7a9f72..48ac7df91e 100644 --- a/music21/analysis/windowed.py +++ b/music21/analysis/windowed.py @@ -349,9 +349,6 @@ def process(self, # ----------------------------------------------------------------------------- -class TestExternal(unittest.TestCase): - pass - class TestMockProcessor: @@ -363,6 +360,9 @@ def process(self, subStream): class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) def testBasic(self): from music21 import corpus diff --git a/music21/articulations.py b/music21/articulations.py index 7c3cccb048..a5b331a2d8 100644 --- a/music21/articulations.py +++ b/music21/articulations.py @@ -662,6 +662,10 @@ class HandbellIndication(TechnicalIndication): # ------------------------------------------------------------------------------ class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) + def testBasic(self): a = FretBend() diff --git a/music21/bar.py b/music21/bar.py index 2756c1ea57..cc774a0bbe 100644 --- a/music21/bar.py +++ b/music21/bar.py @@ -375,6 +375,9 @@ def getTextExpression(self, prefix='', postfix='x'): # ------------------------------------------------------------------------------ class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) def testSortOrder(self): from music21 import stream diff --git a/music21/base.py b/music21/base.py index ad4debdbbc..7c59167652 100644 --- a/music21/base.py +++ b/music21/base.py @@ -27,7 +27,7 @@ >>> music21.VERSION_STR -'9.0.0a4' +'9.0.0a5' Alternatively, after doing a complete import, these classes are available under the module "base": @@ -456,108 +456,48 @@ def mergeAttributes(self, other: 'Music21Object') -> None: self.id = other.id self.groups = copy.deepcopy(other.groups) - # PyCharm 2019 does not know that copy.deepcopy can take a memo argument - # noinspection PyArgumentList def _deepcopySubclassable(self: _M21T, - memo=None, - ignoreAttributes=None) -> _M21T: + memo: dict[int, t.Any] | None = None, + *, + ignoreAttributes: set[str] | None = None) -> _M21T: ''' Subclassable __deepcopy__ helper so that the same attributes - do not need to be called - for each Music21Object subclass. + do not need to be called for each Music21Object subclass. ignoreAttributes is a set of attributes not to copy via - the default deepcopy style. - More can be passed to it. - - TODO: move to class attributes to cache. + the default deepcopy style. More can be passed to it. But calling + functions are responsible Changed in v9: removeFromIgnore removed; never used and this is performance critical. ''' - defaultIgnoreSet = {'_derivation', '_activeSite', - 'sites', '_duration', '_style', '_cache'} + defaultIgnoreSet = {'_derivation', '_activeSite', '_sites', '_cache'} + if not self.groups: + defaultIgnoreSet.add('groups') + # duration is smart enough to do itself. + # sites is smart enough to do itself + if ignoreAttributes is None: ignoreAttributes = defaultIgnoreSet else: ignoreAttributes = ignoreAttributes | defaultIgnoreSet - # call class to get a new, empty instance - # TODO: this creates an extra duration object for notes... optimize... - if '_duration' in ignoreAttributes and self._duration is not None: - d = self._duration - clientStore = d.client - d.client = None - newDuration = copy.deepcopy(d, memo) - d.client = clientStore - new = self.__class__(duration=newDuration) - else: - new = self.__class__() - - if '_derivation' in ignoreAttributes: - # was: keep the old ancestor but need to update the client - # 2.1 : NO, add a derivation of __deepcopy__ to the client - newDerivation = Derivation(client=new) - newDerivation.origin = self - newDerivation.method = '__deepcopy__' - setattr(new, '_derivation', newDerivation) - - if '_activeSite' in ignoreAttributes: - # TODO: Fix this so as not to allow incorrect _activeSite (???) - # keep a reference, not a deepcopy - # do not use property: .activeSite; set to same weakref obj - # TODO: restore jan 2020 (was Jan 2018) - # setattr(new, '_activeSite', None) - setattr(new, '_activeSite', self._activeSite) - - if 'sites' in ignoreAttributes: - # we make a copy of the sites value even though it is obsolete because - # the spanners will need to be preserved and then set to the new value - # elsewhere. The purgeOrphans call later will remove all but - # spanners and variants. - value = getattr(self, 'sites') - # this calls __deepcopy__ in Sites - newValue = copy.deepcopy(value, memo) - setattr(new, 'sites', newValue) - if '_style' in ignoreAttributes: - value = getattr(self, '_style', None) - if value is not None: - newValue = copy.deepcopy(value, memo) - setattr(new, '_style', newValue) - - for name in self.__dict__: - if name.startswith('__'): - continue - if name in ignoreAttributes: - continue + new = common.defaultDeepcopy(self, memo, ignoreAttributes=ignoreAttributes) + setattr(new, '_cache', {}) + setattr(new, '_sites', Sites()) + if 'groups' in defaultIgnoreSet: + new.groups = Groups() - attrValue = getattr(self, name) - # attributes that do not require special handling - try: - deeplyCopiedObject = copy.deepcopy(attrValue, memo) - setattr(new, name, deeplyCopiedObject) - except TypeError as te: # pragma: no cover - if not isinstance(attrValue, Music21Object): - # shallow copy then... - try: - shallowlyCopiedObject = copy.copy(attrValue) - setattr(new, name, shallowlyCopiedObject) - environLocal.printDebug( - '__deepcopy__: Could not deepcopy ' - + f'{name} in {self}, not a Music21Object' - + 'so making a shallow copy') - except TypeError: - # just link... - environLocal.printDebug( - '__deepcopy__: Could not copy (deep or shallow) ' - + f'{name} in {self}, not a Music21Object so just making a link' - ) - setattr(new, name, attrValue) - else: # raise error for our own problem. # pragma: no cover - raise Music21Exception( - '__deepcopy__: Cannot deepcopy Music21Object ' - + f'{name} probably because it requires a default value in instantiation.' - ) from te + # was: keep the old ancestor but need to update the client + # 2.1 : NO, add a derivation of __deepcopy__ to the client + newDerivation = Derivation(client=new) + newDerivation.origin = self + newDerivation.method = '__deepcopy__' + setattr(new, '_derivation', newDerivation) + # None activeSite is correct for new value + + # must do this after copying + new.purgeOrphans() return new @@ -600,12 +540,7 @@ def __deepcopy__(self: _M21T, memo: dict[int, t.Any] | None = None) -> _M21T: >>> ('flute' in n.groups, 'flute' in b.groups) (False, True) ''' - # environLocal.printDebug(['calling Music21Object.__deepcopy__', self]) - new = self._deepcopySubclassable(memo) - # must do this after copying - new.purgeOrphans() - # environLocal.printDebug([self, 'end deepcopy', 'self._activeSite', self._activeSite]) - return new + return self._deepcopySubclassable(memo) def __getstate__(self) -> dict[str, t.Any]: state = self.__dict__.copy() @@ -1804,7 +1739,7 @@ def contextSites( indices to ensure that no other temporary streams are created; normally, we would do `c.parts['#Alto'].measure(3)`. - >>> m = c[2][4] + >>> m = c.parts['#Alto'].getElementsByClass(stream.Measure)[3] >>> m @@ -1829,11 +1764,10 @@ def contextSites( (, '9.0 <0.-20...>', ) (, '9.0 <0.-20...>', ) - Here we make a copy of the earlier measure, and we see that its contextSites follow the derivationChain from the original measure and still find the Part - and Score of the original Measure 3 even though mCopy is not in any of these - objects. + and Score of the original Measure 3 (and also the original Measure 3) + even though mCopy is not in any of these objects. >>> import copy >>> mCopy = copy.deepcopy(m) @@ -1843,6 +1777,9 @@ def contextSites( ContextTuple(site=, offset=0.0, recurseType=) False + ContextTuple(site=, + offset=0.0, + recurseType=) False ContextTuple(site=, offset=9.0, recurseType=) False @@ -1955,7 +1892,7 @@ def contextSites( from music21 import stream if memo is None: - memo = [] + memo = set() if callerFirst is None: callerFirst = self @@ -1972,7 +1909,7 @@ def contextSites( yield ContextSortTuple(streamSelf, selfSortTuple, recursionType) else: yield ContextTuple(streamSelf, 0.0, recursionType) - memo.append(streamSelf) + memo.add(streamSelf) if priorityTarget is None and sortByCreationTime is False: priorityTarget = self.activeSite @@ -2009,7 +1946,7 @@ def contextSites( else: yield ContextTuple(siteObj, positionInStream.offset, recursionType) - memo.append(siteObj) + memo.add(siteObj) environLocal.printDebug( f'looking in contextSites for {siteObj}' + f' with position {positionInStream.shortRepr()}') @@ -2037,7 +1974,7 @@ def contextSites( yield ContextSortTuple(topLevel, hypotheticalPosition, recurType) else: yield ContextTuple(topLevel, inStreamOffset, recurType) - memo.append(topLevel) + memo.add(topLevel) if priorityTargetOnly: break @@ -2070,7 +2007,7 @@ def contextSites( yield ContextTuple(offsetAdjustedCsTuple.site, offsetAdjustedCsTuple.offset.offset, offsetAdjustedCsTuple.recurseType) - memo.append(derivedCsTuple.site) + memo.add(derivedCsTuple.site) environLocal.printDebug('--returning from derivedObject search') diff --git a/music21/beam.py b/music21/beam.py index 0470645f7b..7f39256471 100644 --- a/music21/beam.py +++ b/music21/beam.py @@ -702,13 +702,14 @@ def setByNumber(self, number, type, direction=None): # type is okay @ReservedAs class Test(unittest.TestCase): - pass + + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) # ----------------------------------------------------------------------------- # define presented order in documentation - - _DOC_ORDER = [Beams, Beam] diff --git a/music21/braille/objects.py b/music21/braille/objects.py index dc4cab3c33..c412bd2d8f 100644 --- a/music21/braille/objects.py +++ b/music21/braille/objects.py @@ -79,7 +79,9 @@ class BrailleExplicitNoteExtraSmaller(BrailleExplicitNoteLength): # ------------------------------------------------------------------------------ class Test(unittest.TestCase): - pass + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) if __name__ == '__main__': diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index 54d0f65f68..5e67ceee39 100644 --- a/music21/chord/__init__.py +++ b/music21/chord/__init__.py @@ -90,6 +90,7 @@ def __init__(self, str, Sequence[str], Sequence[pitch.Pitch], + Sequence[ChordBase], Sequence[note.NotRest], Sequence[int]] = None, **keywords): @@ -727,6 +728,7 @@ def __init__(self, notes: t.Union[None, Sequence[pitch.Pitch], Sequence[note.Note], + Sequence[Chord], Sequence[str], str, Sequence[int]] = None, diff --git a/music21/common/misc.py b/music21/common/misc.py index 5ba186654a..71d4c87488 100644 --- a/music21/common/misc.py +++ b/music21/common/misc.py @@ -22,7 +22,9 @@ import sys import textwrap import time +import types import typing as t +import weakref __all__ = [ 'flattenList', @@ -231,7 +233,16 @@ def runningUnderIPython() -> bool: # NB -- temp files (tempFile) etc. are in environment.py # ------------------------------------------------------------------------------ -def defaultDeepcopy(obj, memo, callInit=True): +# From copy.py +_IMMUTABLE_DEEPCOPY_TYPES = { + type(None), type(Ellipsis), type(NotImplemented), + int, float, bool, complex, bytes, str, + types.CodeType, type, range, + types.BuiltinFunctionType, types.FunctionType, + weakref.ref, property, +} + +def defaultDeepcopy(obj: t.Any, memo=None, *, ignoreAttributes: Iterable[str] = ()): ''' Unfortunately, it is not possible to do something like:: @@ -251,31 +262,27 @@ def __deepcopy__(self, memo): else: return common.defaultDeepcopy(self, memo) - looks through both __slots__ and __dict__ and does a deepcopy - of anything in each of them and returns the new object. + Does a deepcopy of the state returned by `__reduce_ex__` for protocol 4. - If callInit is False, then only __new__() is called. This is - much faster if you're just going to overload every instance variable - or is required if __init__ has required variables in initialization. + * Changed in v9: callInit is removed, replaced with ignoreAttributes. + uses `__reduce_ex__` and `copy._reconstruct` internally. ''' - if callInit is False: - new = obj.__class__.__new__(obj.__class__) - else: - new = obj.__class__() - - dictState = getattr(obj, '__dict__', None) - if dictState is not None: - for k in dictState: - # noinspection PyArgumentList - setattr(new, k, copy.deepcopy(dictState[k], memo=memo)) - slots = set() - for cls in obj.__class__.mro(): # it is okay that it's in reverse order, since it's just names - slots.update(getattr(cls, '__slots__', ())) - for slot in slots: - slotValue = getattr(obj, slot, None) - # might be none if slot was deleted; it will be recreated here - setattr(new, slot, copy.deepcopy(slotValue)) - + if memo is None: + memo = {} + + rv = obj.__reduce_ex__(4) # get a protocol 4 reduction + func, args, state = rv[:3] + new = func(*args) + memo[id(obj)] = new + + # set up reducer to not copy the ignoreAttributes set. + for attr, value in state.items(): + if attr in ignoreAttributes: + setattr(new, attr, None) + elif type(value) in _IMMUTABLE_DEEPCOPY_TYPES: + setattr(new, attr, value) + else: + setattr(new, attr, copy.deepcopy(value, memo)) return new diff --git a/music21/corpus/_metadataCache/core.p.gz b/music21/corpus/_metadataCache/core.p.gz index d11913bd17..b14d6f82c5 100644 Binary files a/music21/corpus/_metadataCache/core.p.gz and b/music21/corpus/_metadataCache/core.p.gz differ diff --git a/music21/duration.py b/music21/duration.py index 5026c8ab8b..efd7483517 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -907,7 +907,6 @@ def durationTupleFromQuarterLength(ql=1.0) -> DurationTuple: return DurationTuple('inexpressible', 0, ql) -@lru_cache(1024) def durationTupleFromTypeDots(durType='quarter', dots=0): ''' Returns a DurationTuple (which knows its quarterLength) for @@ -1669,7 +1668,7 @@ def __init__(self, /, *, type: str | None = None, # pylint: disable=redefined-builtin - dots: int | None = None, + dots: int | None = 0, quarterLength: OffsetQLIn | None = None, durationTuple: DurationTuple | None = None, components: Iterable[DurationTuple] | None = None, @@ -1713,12 +1712,7 @@ def __init__(self, ) if durationTuple is not None: - self.addDurationTuple(durationTuple) - - if dots is not None: - storeDots = dots - else: - storeDots = 0 + self.addDurationTuple(durationTuple, _skipInform=True) if components is not None: self.components = t.cast(tuple[DurationTuple, ...], components) @@ -1726,8 +1720,8 @@ def __init__(self, # self._quarterLengthNeedsUpdating = True if type is not None: - nt = durationTupleFromTypeDots(type, storeDots) - self.addDurationTuple(nt) + nt = durationTupleFromTypeDots(type, dots) + self.addDurationTuple(nt, _skipInform=True) # permit as keyword so can be passed from notes elif quarterLength is not None: self.quarterLength = quarterLength @@ -1808,27 +1802,27 @@ def _reprInternal(self): else: return f'unlinked type:{self.type} quarterLength:{self.quarterLength}' - # unwrap weakref for pickling def __deepcopy__(self, memo): ''' - Do some very fast creations... + Don't copy client when creating ''' - if (self._componentsNeedUpdating is False - and len(self._components) == 1 + if self._componentsNeedUpdating: + self._updateComponents() + + if (len(self._components) == 1 and self._dotGroups == (0,) and self._linked is True and not self._tuplets): # 99% of notes... # ignore all but components return self.__class__(durationTuple=self._components[0]) - elif (self._componentsNeedUpdating is False - and not self._components - and self._dotGroups == (0,) - and not self._tuplets - and self._linked is True): + elif (not self._components + and self._dotGroups == (0,) + and not self._tuplets + and self._linked is True): # ignore all return self.__class__() else: - return common.defaultDeepcopy(self, memo) + return common.defaultDeepcopy(self, memo, ignoreAttributes={'client'}) # PRIVATE METHODS # @@ -1886,7 +1880,10 @@ def _setLinked(self, value: bool): linked = property(_getLinked, _setLinked) - def addDurationTuple(self, dur: DurationTuple | Duration | str | OffsetQLIn): + def addDurationTuple(self, + dur: DurationTuple | Duration | str | OffsetQLIn, + *, + _skipInform=False): ''' Add a DurationTuple or a Duration's components to this Duration. Does not simplify the Duration. For instance, adding two @@ -1918,7 +1915,8 @@ def addDurationTuple(self, dur: DurationTuple | Duration | str | OffsetQLIn): if self.linked: self._quarterLengthNeedsUpdating = True - self.informClient() + if not _skipInform: + self.informClient() def appendTuplet(self, newTuplet: Tuplet) -> None: ''' @@ -3110,6 +3108,11 @@ def __init__(self, *arguments, **keywords): self._updateComponents() self._updateQuarterLength() + def __deepcopy__(self, memo=None): + ''' + Immutable objects return themselves + ''' + return self class GraceDuration(Duration): ''' diff --git a/music21/features/base.py b/music21/features/base.py index 330df373ef..8326e2ef02 100644 --- a/music21/features/base.py +++ b/music21/features/base.py @@ -1279,7 +1279,6 @@ def getIndex(featureString, extractorType=None): class Test(unittest.TestCase): def testStreamFormsA(self): - from music21 import features self.maxDiff = None diff --git a/music21/freezeThaw.py b/music21/freezeThaw.py index 13b3abef63..d2d03cef62 100644 --- a/music21/freezeThaw.py +++ b/music21/freezeThaw.py @@ -75,7 +75,6 @@ import pickle import time import unittest -import weakref import zlib from music21 import base @@ -314,7 +313,7 @@ def removeStreamStatusClient(self, streamObj): streamObj (not recursive). Called by setupSerializationScaffold. ''' if hasattr(streamObj, 'streamStatus'): - streamObj.streamStatus._client = None + streamObj.streamStatus.client = None def recursiveClearSites(self, startObj): ''' @@ -1214,6 +1213,8 @@ def testJSONPickleSpanner(self): def testPickleMidi(self): from music21 import converter + from music21 import note + a = str(common.getSourceFilePath() / 'midi' / 'testPrimitive' @@ -1224,8 +1225,8 @@ def testPickleMidi(self): f = converter.freezeStr(c) d = converter.thawStr(f) self.assertIsInstance( - d.parts[1].flatten().notes[20].volume._client, - weakref.ReferenceType) + d.parts[1].flatten().notes[20].volume.client, + note.NotRest) # ----------------------------------------------------------------------------- diff --git a/music21/humdrum/spineParser.py b/music21/humdrum/spineParser.py index 9e98b0cca5..adf8d80630 100644 --- a/music21/humdrum/spineParser.py +++ b/music21/humdrum/spineParser.py @@ -685,7 +685,6 @@ def insertGlobalEvents(self): Insert the Global Events (GlobalReferenceLines and GlobalCommentLines) into an appropriate place in the outer Stream. - Run after self.spineCollection.createMusic21Streams(). Is run automatically by self.parse(). uses self.spineCollection.getOffsetsAndPrioritiesByPosition() @@ -737,31 +736,31 @@ def insertGlobalEvents(self): if appendList: self.stream.coreElementsChanged() -# @property -# def stream(self): -# if self._storedStream is not None: -# return self._storedStream -# if self.parsedLines is False: -# self.parse() -# -# if self.spineCollection is None: -# raise HumdrumException('parsing got no spine collections!') -# elif self.spineCollection.spines is None: -# raise HumdrumException('not a single spine in your data... um, not my problem! ' + -# '(well, maybe it is...file a bug report if you ' + -# 'have doubled checked your data)') -# elif self.spineCollection.spines[0].stream is None: -# raise HumdrumException('okay, you got at least one spine, but it ain\'t got ' + -# 'a stream in it; (check your data or file a bug report)') -# else: -# masterStream = stream.Score() -# for thisSpine in self.spineCollection: -# thisSpine.stream.id = 'spine_' + str(thisSpine.id) -# for thisSpine in self.spineCollection: -# if thisSpine.parentSpine is None and thisSpine.spineType == 'kern': -# masterStream.insert(thisSpine.stream) -# self._storedStream = masterStream -# return masterStream + # @property + # def stream(self): + # if self._storedStream is not None: + # return self._storedStream + # if self.parsedLines is False: + # self.parse() + # + # if self.spineCollection is None: + # raise HumdrumException('parsing got no spine collections!') + # elif self.spineCollection.spines is None: + # raise HumdrumException('not a single spine in your data... um, not my problem! ' + + # '(well, maybe it is...file a bug report if you ' + + # 'have doubled checked your data)') + # elif self.spineCollection.spines[0].stream is None: + # raise HumdrumException('okay, you got at least one spine, but it ain\'t got ' + + # 'a stream in it; (check your data or file a bug report)') + # else: + # masterStream = stream.Score() + # for thisSpine in self.spineCollection: + # thisSpine.stream.id = 'spine_' + str(thisSpine.id) + # for thisSpine in self.spineCollection: + # if thisSpine.parentSpine is None and thisSpine.spineType == 'kern': + # masterStream.insert(thisSpine.stream) + # self._storedStream = masterStream + # return masterStream def parseMetadata(self, s=None): ''' @@ -1439,7 +1438,6 @@ class DynamSpine(HumdrumSpine): attribute set and thus events are processed as if they are dynamics. ''' - def parse(self): thisContainer = None for event in self.eventList: @@ -2504,7 +2502,6 @@ def kernTandemToObject(tandem): >>> m - Unknown objects are converted to MiscTandem objects: >>> m2 = humdrum.spineParser.kernTandemToObject('*TandyUnk') @@ -2619,8 +2616,8 @@ def kernTandemToObject(tandem): class MiscTandem(base.Music21Object): - def __init__(self, tandem=''): - super().__init__() + def __init__(self, tandem='', **keywords): + super().__init__(**keywords) self.tandem = tandem def _reprInternal(self): @@ -2638,9 +2635,8 @@ class SpineComment(base.Music21Object): >>> sc.comment 'this is a spine comment' ''' - - def __init__(self, comment=''): - super().__init__() + def __init__(self, comment='', **keywords): + super().__init__(**keywords) commentPart = re.sub(r'^!+\s?', '', comment) self.comment = commentPart @@ -2652,16 +2648,14 @@ class GlobalComment(base.Music21Object): ''' A Music21Object that represents a comment for the whole score - >>> sc = humdrum.spineParser.GlobalComment('!! this is a global comment') >>> sc >>> sc.comment 'this is a global comment' ''' - - def __init__(self, comment=''): - super().__init__() + def __init__(self, comment='', **keywords): + super().__init__(**keywords) commentPart = re.sub(r'^!!+\s?', '', comment) commentPart = commentPart.strip() self.comment = commentPart @@ -2712,9 +2706,8 @@ class GlobalReference(base.Music21Object): >>> sc.isPrimary False ''' - - def __init__(self, codeOrAll='', valueOrNone=None): - super().__init__() + def __init__(self, codeOrAll='', valueOrNone=None, **keywords): + super().__init__(**keywords) codeOrAll = re.sub(r'^!!!+', '', codeOrAll) codeOrAll = codeOrAll.strip() if valueOrNone is None and ':' in codeOrAll: @@ -2816,7 +2809,7 @@ def __init__(self, codeOrAll='', valueOrNone=None): 'EED': 'electronicEditor', # electronic editor 'ENC': 'electronicEncoder', # electronic encoder (person) 'END': '', # encoding date - 'EMD': '', # electronic document modification description (one per modificiation) + 'EMD': '', # electronic document modification description (one per modification) 'EEV': '', # electronic edition version 'EFL': '', # file number e.g. '1/4' for one of four 'EST': '', # encoding status (free form, normally eliminated prior to distribution) @@ -2864,6 +2857,9 @@ def _reprInternal(self): class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) def testLoadMazurka(self): # hf1 = HumdrumFile('d:/web/eclipse/music21misc/mazurka06-2.krn') @@ -2871,22 +2867,22 @@ def testLoadMazurka(self): hf1 = HumdrumDataCollection(testFiles.mazurka6) hf1.parse() - # hf1 = HumdrumFile('d:/web/eclipse/music21misc/ojibway.krn') - # for thisEventCollection in hf1.eventCollections: - # ev = thisEventCollection.getSpineEvent(0).contents - # if ev is not None: - # print(ev) - # else: - # print('NONE') - - # for mySpine in hf1.spineCollection: - # print('\n\n***NEW SPINE: No. ' + str(mySpine.id) + ' parentSpine: ' - # + str(mySpine.parentSpine) + ' childSpines: ' + str(mySpine.childSpines)) - # print(mySpine.spineType) - # for childSpinesSpine in mySpine.childSpinesSpines(): - # print(str(childSpinesSpine.id) + ' *** testing spineCollection code ***') - # for thisEvent in mySpine: - # print(thisEvent.contents) + # hf1 = HumdrumFile('d:/web/eclipse/music21misc/ojibway.krn') + # for thisEventCollection in hf1.eventCollections: + # ev = thisEventCollection.getSpineEvent(0).contents + # if ev is not None: + # print(ev) + # else: + # print('NONE') + # + # for mySpine in hf1.spineCollection: + # print('\n\n***NEW SPINE: No. ' + str(mySpine.id) + ' parentSpine: ' + # + str(mySpine.parentSpine) + ' childSpines: ' + str(mySpine.childSpines)) + # print(mySpine.spineType) + # for childSpinesSpine in mySpine.childSpinesSpines(): + # print(str(childSpinesSpine.id) + ' *** testing spineCollection code ***') + # for thisEvent in mySpine: + # print(thisEvent.contents) spine5 = hf1.spineCollection.getSpineById(5) self.assertEqual(spine5.id, 5) self.assertEqual(spine5.parentSpine.id, 1) diff --git a/music21/note.py b/music21/note.py index 2cfdcf8fca..b538a23701 100644 --- a/music21/note.py +++ b/music21/note.py @@ -44,6 +44,7 @@ from music21 import chord from music21 import instrument from music21 import percussion + _NotRestType = t.TypeVar('_NotRestType', bound='NotRest') environLocal = environment.Environment('note') @@ -1019,6 +1020,19 @@ def __eq__(self, other): return False return True + def _deepcopySubclassable(self: _NotRestType, + memo: dict[int, t.Any] | None = None, + *, + ignoreAttributes: set[str] | None = None) -> _NotRestType: + new = super()._deepcopySubclassable(memo, ignoreAttributes={'_chordAttached'}) + # let the chord restore _chordAttached + + # after copying, if a Volume exists, it is linked to the old object + # look at _volume so as not to create object if not already there + if self.hasVolumeInformation(): + new.volume.client = new # update with new instance + return new + def __deepcopy__(self, memo=None): ''' As NotRest objects have a Volume, objects, and Volume objects @@ -1032,25 +1046,7 @@ def __deepcopy__(self, memo=None): True ''' # environLocal.printDebug(['calling NotRest.__deepcopy__', self]) - new = super().__deepcopy__(memo=memo) - # after copying, if a Volume exists, it is linked to the old object - # look at _volume so as not to create object if not already there - # pylint: disable=no-member - if self._volume is not None: - new.volume.client = new # update with new instance - return new - - def __getstate__(self): - state = super().__getstate__() - if '_volume' in state and state['_volume'] is not None: - state['_volume'].client = None - return state - - def __setstate__(self, state): - super().__setstate__(state) - if self._volume is not None: - self._volume.client = self - #### + return self._deepcopySubclassable(memo=memo) def _getStemDirection(self) -> str: return self._stemDirection @@ -1225,7 +1221,7 @@ def hasVolumeInformation(self) -> bool: return True def _getVolume(self, - forceClient: base.Music21Object | None = None + forceClient: NotRest | None = None ) -> volume.Volume: # DO NOT CHANGE TO @property because of optional attributes # lazy volume creation. property is set below. @@ -1591,7 +1587,7 @@ def __deepcopy__(self: Note, memo=None) -> Note: ''' After doing a deepcopy of the pitch, be sure to set the client ''' - new = super().__deepcopy__(memo=memo) + new = self._deepcopySubclassable(memo) # noinspection PyProtectedMember new.pitch._client = new # pylint: disable=no-member return new diff --git a/music21/pitch.py b/music21/pitch.py index 96ef11767f..df2818b29b 100644 --- a/music21/pitch.py +++ b/music21/pitch.py @@ -1585,7 +1585,8 @@ class Pitch(prebase.ProtoM21Object): Sometimes we need an octave for a `Pitch` even if it's not specified. For instance, we can't play an octave-less `Pitch` in MIDI or display it on a staff. So there is an `.implicitOctave` - tag to deal with these situations; by default it's always 4. + tag to deal with these situations; by default it's always 4 (unless + defaults.pitchOctave is changed) >>> anyGSharp.implicitOctave 4 @@ -1596,7 +1597,6 @@ class Pitch(prebase.ProtoM21Object): >>> highEflat.implicitOctave 6 - If an integer or float >= 12 is passed to the constructor then it is used as the `.ps` attribute, which is for most common piano notes, the same as a MIDI number: @@ -1830,11 +1830,12 @@ def __init__(self, # 5% of pitch creation time; it'll be created in a sec anyhow self._microtone: Microtone | None = None - # CA, Q: should this remain an attribute or only refer to value in defaults? - # MSC A: no, it's a useful attribute for cases such as scales where if there are - # no octaves we give a defaultOctave higher than the previous - # (MSC 12 years later: maybe Chris was right...) - self.defaultOctave: int = defaults.pitchOctave + # # CA, Q: should this remain an attribute or only refer to value in defaults? + # # MSC A: no, it's a useful attribute for cases such as scales where if there are + # # no octaves we give a defaultOctave higher than the previous + # # MSC 12 years later: maybe Chris was right... + # self.defaultOctave: int = defaults.pitchOctave + # # MSC: even later: Chris Ariza was right self._octave: int | None = None # if True, accidental is not known; is determined algorithmically @@ -1919,7 +1920,6 @@ def __eq__(self, other): >>> d = pitch.Pitch('d-4') >>> b == d False - ''' if other is None: return False @@ -1946,11 +1946,13 @@ def __deepcopy__(self, memo): new = Pitch.__new__(Pitch) for k in self.__dict__: v = getattr(self, k, None) - if k in ('_step', '_overridden_freq440', 'defaultOctave', + if k in ('_step', '_overridden_freq440', '_octave', 'spellingIsInferred'): setattr(new, k, v) elif k == '_client': setattr(new, k, None) + elif v is None: # common -- save time over deepcopy. + setattr(new, k, None) else: setattr(new, k, copy.deepcopy(v, memo)) return new @@ -2465,7 +2467,7 @@ def ps(self): >>> d.ps 63.0 - >>> d.defaultOctave = 5 + >>> d.octave = 5 >>> d.ps 75.0 @@ -2489,9 +2491,6 @@ def ps(self): The property is called when self.step, self.octave or self.accidental are changed. - - Should be called when .defaultOctave is changed if octave is None, - but isn't yet. ''' step = self._step ps = float(((self.implicitOctave + 1) * 12) + STEPREF[step]) @@ -3078,7 +3077,7 @@ def implicitOctave(self) -> int: Cannot be set. Instead, just change the `.octave` of the pitch ''' if self.octave is None: - return self.defaultOctave + return defaults.pitchOctave else: return self.octave diff --git a/music21/prebase.py b/music21/prebase.py index 20b1f31210..0c0dac7176 100644 --- a/music21/prebase.py +++ b/music21/prebase.py @@ -283,6 +283,10 @@ def _reprInternal(self) -> str: class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) + def test_reprInternal(self): from music21.base import Music21Object b = Music21Object() diff --git a/music21/romanText/translate.py b/music21/romanText/translate.py index 60c070a053..9034e39d1c 100644 --- a/music21/romanText/translate.py +++ b/music21/romanText/translate.py @@ -167,8 +167,8 @@ class RomanTextUnprocessedToken(base.ElementWrapper): class RomanTextUnprocessedMetadata(base.Music21Object): - def __init__(self, tag='', data=''): - super().__init__() + def __init__(self, tag='', data='', **keywords): + super().__init__(**keywords) self.tag = tag self.data = data diff --git a/music21/scale/__init__.py b/music21/scale/__init__.py index 7bc427a1a9..da66161f9b 100644 --- a/music21/scale/__init__.py +++ b/music21/scale/__init__.py @@ -413,10 +413,10 @@ def fixDefaultOctaveForPitchList(pitchList): for p in pitchList: if p.octave is None: if lastPs > p.ps: - p.defaultOctave = lastOctave + p.octave = lastOctave while lastPs > p.ps: lastOctave += 1 - p.defaultOctave = lastOctave + p.octave = lastOctave lastPs = p.ps lastOctave = p.implicitOctave diff --git a/music21/search/serial.py b/music21/search/serial.py index b536fa578c..c2bc326d31 100644 --- a/music21/search/serial.py +++ b/music21/search/serial.py @@ -46,7 +46,6 @@ class ContiguousSegmentOfNotes(base.Music21Object): >>> cdContiguousSegment = search.serial.ContiguousSegmentOfNotes([n1, n2], s, 0) >>> cdContiguousSegment - ''' _DOC_ATTR: dict[str, str] = { 'segment': 'The list of notes and chords in the contiguous segment.', @@ -76,8 +75,8 @@ class ContiguousSegmentOfNotes(base.Music21Object): _DOC_ORDER = ['startMeasureNumber', 'startOffset', 'zeroCenteredTransformationsFromMatched', 'originalCenteredTransformationsFromMatched'] - def __init__(self, segment=None, containerStream=None, partNumber=0): - super().__init__() + def __init__(self, segment=None, containerStream=None, partNumber=0, **keywords): + super().__init__(**keywords) self.segment = segment self.containerStream = containerStream self.partNumber = partNumber diff --git a/music21/sites.py b/music21/sites.py index 194b05a22c..59e3d29014 100644 --- a/music21/sites.py +++ b/music21/sites.py @@ -856,6 +856,7 @@ def purgeLocations(self, rescanIsDead=False): have the element. This results b/c Sites are shallow-copied, and then elements are re-added. + >>> import gc >>> class Mock(base.Music21Object): ... pass >>> aStream = stream.Stream() @@ -866,6 +867,7 @@ def purgeLocations(self, rescanIsDead=False): >>> mySites.add(aStream) >>> mySites.add(bStream) >>> del aStream + >>> numObjectsCollected = gc.collect() # make sure to garbage collect We still have 3 locations -- just because aStream is gone, doesn't make it disappear from sites diff --git a/music21/spanner.py b/music21/spanner.py index 4bee99eb13..7da3046c7a 100644 --- a/music21/spanner.py +++ b/music21/spanner.py @@ -247,29 +247,26 @@ def _reprInternal(self): msg.append(repr(objRef)) return ''.join(msg) - def _deepcopySubclassable(self, memo=None, ignoreAttributes=None): + def _deepcopySubclassable(self, memo=None, *, ignoreAttributes=None): ''' see __deepcopy__ for tests and docs ''' # NOTE: this is a performance critical operation - defaultIgnoreSet = {'_cache', 'spannerStorage'} + defaultIgnoreSet = {'spannerStorage'} if ignoreAttributes is None: ignoreAttributes = defaultIgnoreSet else: ignoreAttributes = ignoreAttributes | defaultIgnoreSet - - new = super()._deepcopySubclassable(memo, ignoreAttributes) - - if 'spannerStorage' in ignoreAttributes: - # there used to be a bug here where spannerStorage would - # try to append twice. I've removed the guardrail here in v7. - # because I'm pretty sure we have solved it. - # disable pylint check until this inheritance bug is solved: - # https://github.com/PyCQA/astroid/issues/457 - # pylint: disable=no-member - for c in self.spannerStorage._elements: - new.spannerStorage.coreAppend(c) - new.spannerStorage.coreElementsChanged(updateIsFlat=False) + new = t.cast(Spanner, + super()._deepcopySubclassable(memo, ignoreAttributes=ignoreAttributes)) + + # we are temporarily putting in the PREVIOUS elements, to replace them later + # with replaceSpannedElement() + new.spannerStorage = type(self.spannerStorage)(client=new) + for c in self.spannerStorage._elements: + new.spannerStorage.coreAppend(c) + # updateIsSorted too? + new.spannerStorage.coreElementsChanged(updateIsFlat=False) return new def __deepcopy__(self, memo=None): @@ -313,14 +310,18 @@ def __deepcopy__(self, memo=None): # this is the same as with Variants def purgeOrphans(self, excludeStorageStreams=True): - self.spannerStorage.purgeOrphans(excludeStorageStreams) + if self.spannerStorage: + # might not be defined in the middle of a deepcopy. + self.spannerStorage.purgeOrphans(excludeStorageStreams) base.Music21Object.purgeOrphans(self, excludeStorageStreams) def purgeLocations(self, rescanIsDead=False): # must override Music21Object to purge locations from the contained # Stream # base method to perform purge on the Stream - self.spannerStorage.purgeLocations(rescanIsDead=rescanIsDead) + if self.spannerStorage: + # might not be defined in the middle of a deepcopy. + self.spannerStorage.purgeLocations(rescanIsDead=rescanIsDead) base.Music21Object.purgeLocations(self, rescanIsDead=rescanIsDead) # -------------------------------------------------------------------------- @@ -2678,8 +2679,8 @@ def testDeepcopyNotesAndSpannerInStream(self): from music21 import stream from music21.spanner import Spanner - n1 = note.Note('g') - n2 = note.Note('f#') + n1 = note.Note('G4') + n2 = note.Note('F#4') sp1 = Spanner(n1, n2) st1 = stream.Stream() @@ -2687,7 +2688,6 @@ def testDeepcopyNotesAndSpannerInStream(self): st1.insert(0.0, n1) st1.insert(1.0, n2) st2 = copy.deepcopy(st1) - n3 = st2.notes[0] self.assertEqual(len(n3.getSpannerSites()), 1) sp2 = n3.getSpannerSites()[0] diff --git a/music21/stream/base.py b/music21/stream/base.py index f2137353c4..90dbaed084 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -1952,12 +1952,13 @@ class or subclass in the Stream in place. # pylint: disable=no-member def _deepcopySubclassable(self: StreamType, - memo=None, + memo: dict[int, t.Any] | None = None, + *, ignoreAttributes=None, ) -> StreamType: # NOTE: this is a performance critical operation defaultIgnoreSet = { - '_offsetDict', 'streamStatus', '_elements', '_endElements', '_cache', + '_offsetDict', '_elements', '_endElements', } if ignoreAttributes is None: ignoreAttributes = defaultIgnoreSet @@ -1966,53 +1967,53 @@ def _deepcopySubclassable(self: StreamType, # PyCharm seems to think that this is a StreamCore # noinspection PyTypeChecker - new: StreamType = super()._deepcopySubclassable(memo, ignoreAttributes) + new: StreamType = super()._deepcopySubclassable(memo, ignoreAttributes=ignoreAttributes) # new._offsetDict will get filled when ._elements is copied. newOffsetDict: dict[int, tuple[OffsetQLSpecial, base.Music21Object]] = {} new._offsetDict = newOffsetDict + new._elements = [] + new._endElements = [] - if 'streamStatus' in ignoreAttributes: - # update the client - # self.streamStatus.client = None - newStreamStatus = copy.deepcopy(self.streamStatus) - newStreamStatus.client = new - setattr(new, 'streamStatus', newStreamStatus) - # self.streamStatus.client = storedClient - if '_elements' in ignoreAttributes: - # must manually add elements to new Stream - for e in self._elements: - # environLocal.printDebug(['deepcopy()', e, 'old', old, 'id(old)', id(old), - # 'new', new, 'id(new)', id(new), 'old.hasElement(e)', old.hasElement(e), - # 'e.activeSite', e.activeSite, 'e.getSites()', e.getSites(), 'e.getSiteIds()', - # e.getSiteIds()], format='block') - # - # this will work for all with __deepcopy___ - # get the old offset from the activeSite Stream - # user here to provide new offset - # - # new.insert(e.getOffsetBySite(old), newElement, - # ignoreSort=True) - offset = self.elementOffset(e) - if not e.isStream: - # noinspection PyArgumentList - newElement = copy.deepcopy(e, memo) - else: # this prevents needing to make multiple replacements of spanner bundles - newElement = e._deepcopySubclassable(memo) - - # ## TEST on copying!!!! - # if isinstance(newElement, note.Note): - # newElement.pitch.ps += 2.0 - new.coreInsert(offset, newElement, ignoreSort=True) - if '_endElements' in ignoreAttributes: - # must manually add elements to - for e in self._endElements: - # this will work for all with __deepcopy___ - # get the old offset from the activeSite Stream - # user here to provide new offset + # streamStatus's deepcopy is smart enough to ignore client. set new + new.streamStatus.client = new + # must manually add elements to new Stream + for e in self._elements: + # environLocal.printDebug(['deepcopy()', e, 'old', old, 'id(old)', id(old), + # 'new', new, 'id(new)', id(new), 'old.hasElement(e)', old.hasElement(e), + # 'e.activeSite', e.activeSite, 'e.getSites()', e.getSites(), 'e.getSiteIds()', + # e.getSiteIds()], format='block') + # + # this will work for all with __deepcopy___ + # get the old offset from the activeSite Stream + # user here to provide new offset + # + # new.insert(e.getOffsetBySite(old), newElement, + # ignoreSort=True) + offset = self.elementOffset(e) + if not e.isStream: # noinspection PyArgumentList - new.coreStoreAtEnd(copy.deepcopy(e, memo)) + newElement = copy.deepcopy(e, memo) + else: + # this prevents needing to make multiple replacements of spanner bundles + # apparently that was at some point a HUGE slowdown. not 100% sure if + # it is still a problem or why. + newElement = e._deepcopySubclassable(memo) + + # ## TEST on copying!!!! + # if isinstance(newElement, note.Note): + # newElement.pitch.ps += 2.0 + new.coreInsert(offset, newElement, ignoreSort=True) + + # must manually add elements to + for e in self._endElements: + # this will work for all with __deepcopy___ + # get the old offset from the activeSite Stream + # user here to provide new offset + + # noinspection PyArgumentList + new.coreStoreAtEnd(copy.deepcopy(e, memo)) new.coreElementsChanged() @@ -2022,15 +2023,11 @@ def __deepcopy__(self: StreamType, memo=None) -> StreamType: ''' Deepcopy the stream from copy.deepcopy() ''' - # does not purgeOrphans -- q: is that a bug or by design? new = self._deepcopySubclassable(memo) + # see custom _deepcopySubclassable for why this is here rather than there. if new._elements: # pylint: disable:no-member self._replaceSpannerBundleForDeepcopy(new) - # purging these orphans works in nearly all cases, but there are a few - # cases where we rely on a Stream having access to Stream it was - # part of after deepcopying - # new.purgeOrphans() return new def _replaceSpannerBundleForDeepcopy(self, new): @@ -4303,10 +4300,98 @@ def getElementAfterElement(self, element, classList=None): # ------------------------------------------------------------------------- # routines for obtaining specific types of elements from a Stream # _getNotes and _getPitches are found with the interval routines + def _getMeasureNumberListByStartEnd( + self, + numberStart, + numberEnd, + *, + indicesNotNumbers: bool + ) -> list[Measure]: + def hasMeasureNumberInformation(measureIterator): + ''' + Many people create streams where every number is zero. + This will check for that as quickly as possible. + ''' + for m in measureIterator: + try: + mNumber = int(m.number) + except ValueError: # pragma: no cover + # should never happen. + raise StreamException(f'found problematic measure for numbering: {m}') + if mNumber != 0: + return True + return False + + mStreamIter: iterator.StreamIterator[Measure] = self.getElementsByClass(Measure) + + # FIND THE CORRECT ORIGINAL MEASURE OBJECTS + # for indicesNotNumbers, this is simple... + if indicesNotNumbers: + # noinspection PyTypeChecker + return t.cast(list[Measure], list(mStreamIter[numberStart:numberEnd])) + + hasUniqueMeasureNumbers = hasMeasureNumberInformation(mStreamIter) + + # unused... + # originalNumberStart = numberStart + # originalNumberEnd = numberEnd + startSuffix = None + endSuffix = None + if isinstance(numberStart, str): + numberStart, startSuffixTemp = common.getNumFromStr(numberStart) + if startSuffixTemp: + startSuffix = startSuffixTemp + numberStart = int(numberStart) + + if isinstance(numberEnd, str): + numberEnd, endSuffixTemp = common.getNumFromStr(numberEnd) + if endSuffixTemp: + endSuffix = endSuffixTemp + numberEnd = int(numberEnd) + + matches: list[Measure] + if numberEnd is not None: + matchingMeasureNumbers = set(range(numberStart, numberEnd + 1)) + + if hasUniqueMeasureNumbers: + matches = [m for m in mStreamIter if m.number in matchingMeasureNumbers] + else: + matches = [m for i, m in enumerate(mStreamIter) + if i + 1 in matchingMeasureNumbers] + else: + if hasUniqueMeasureNumbers: + matches = [m for m in mStreamIter if m.number >= numberStart] + else: + matches = [m for i, m in enumerate(mStreamIter) + if i + 1 >= numberStart] + + if startSuffix is not None: + oldMatches = matches + matches = [] + for m in oldMatches: + if m.number != numberStart: + matches.append(m) + elif not m.numberSuffix: + matches.append(m) + elif m.numberSuffix >= startSuffix: + matches.append(m) + + if endSuffix is not None: + oldMatches = matches + matches = [] + for m in oldMatches: + if m.number != numberEnd: + matches.append(m) + elif not m.numberSuffix: + matches.append(m) + elif m.numberSuffix <= endSuffix: + matches.append(m) + return matches def measures(self, numberStart, numberEnd, + *, collect=('Clef', 'TimeSignature', 'Instrument', 'KeySignature'), gatherSpanners=GatherSpanners.ALL, indicesNotNumbers=False): @@ -4341,7 +4426,6 @@ def measures(self, >>> firstExcerptMeasure.number 1 - To get all measures from the beginning, go ahead and always request measure 0 to x, there will be no error if there is not a pickup measure. @@ -4368,7 +4452,6 @@ def measures(self, 1 - If `numberEnd=None` then it is interpreted as the last measure of the stream: >>> bachExcerpt3 = bachIn.parts[0].measures(7, None) @@ -4412,7 +4495,6 @@ def measures(self, D- major ... - What is collected is determined by the "collect" iterable. To collect nothing send an empty list: @@ -4422,7 +4504,6 @@ def measures(self, - If a stream has measure suffixes, then Streams having that suffix or no suffix are returned. @@ -4459,7 +4540,6 @@ def measures(self, {0.0} - Changed in v.7 -- If `gatherSpanners` is True or GatherSpanners.ALL (default), then just the spanners pertaining to the requested measure region are provided, rather than the entire bundle from the source. @@ -4482,96 +4562,19 @@ def measures(self, ... print(sp) <... P5-Staff2>> <...Alto II>> + + This is in OMIT ''' startMeasure: Measure | None - def hasMeasureNumberInformation(measureIterator): - ''' - Many people create streams where every number is zero. - This will check for that as quickly as possible. - ''' - uniqueMeasureNumbers = set() - for m in measureIterator: - try: - mNumber = int(m.number) - except ValueError: # pragma: no cover - # should never happen. - raise StreamException(f'found problematic measure for numbering: {m}') - uniqueMeasureNumbers.add(mNumber) - if len(uniqueMeasureNumbers) > 1: - break - if len(uniqueMeasureNumbers) > 1: - return True - elif len(uniqueMeasureNumbers.union({0})) > 1: - return True # is there a number other than zero? (or any number at all - else: - return False - returnObj = self.cloneEmpty(derivationMethod='measures') srcObj = self - mStreamIter: iterator.StreamIterator[Measure] = self.getElementsByClass(Measure) - - # FIND THE CORRECT ORIGINAL MEASURE OBJECTS - # for indicesNotNumbers, this is simple... - if indicesNotNumbers: - matches = mStreamIter[numberStart:numberEnd] - else: - hasUniqueMeasureNumbers = hasMeasureNumberInformation(mStreamIter) - - # unused... - # originalNumberStart = numberStart - # originalNumberEnd = numberEnd - startSuffix = None - endSuffix = None - if isinstance(numberStart, str): - numberStart, startSuffixTemp = common.getNumFromStr(numberStart) - if startSuffixTemp: - startSuffix = startSuffixTemp - numberStart = int(numberStart) - - if isinstance(numberEnd, str): - numberEnd, endSuffixTemp = common.getNumFromStr(numberEnd) - if endSuffixTemp: - endSuffix = endSuffixTemp - numberEnd = int(numberEnd) - - if numberEnd is not None: - matchingMeasureNumbers = set(range(numberStart, numberEnd + 1)) - - if hasUniqueMeasureNumbers: - matches = [m for m in mStreamIter if m.number in matchingMeasureNumbers] - else: - matches = [m for i, m in enumerate(mStreamIter) - if i + 1 in matchingMeasureNumbers] - else: - if hasUniqueMeasureNumbers: - matches = [m for m in mStreamIter if m.number >= numberStart] - else: - matches = [m for i, m in enumerate(mStreamIter) - if i + 1 >= numberStart] - - if startSuffix is not None: - oldMatches = matches - matches = [] - for m in oldMatches: - if m.number != numberStart: - matches.append(m) - elif not m.numberSuffix: - matches.append(m) - elif m.numberSuffix >= startSuffix: - matches.append(m) - - if endSuffix is not None: - oldMatches = matches - matches = [] - for m in oldMatches: - if m.number != numberEnd: - matches.append(m) - elif not m.numberSuffix: - matches.append(m) - elif m.numberSuffix <= endSuffix: - matches.append(m) + matches = self._getMeasureNumberListByStartEnd( + numberStart, + numberEnd, + indicesNotNumbers=indicesNotNumbers + ) if not matches: startMeasure = None @@ -4612,8 +4615,10 @@ def hasMeasureNumberInformation(measureIterator): # environLocal.printDebug(['len(returnObj.flatten())', len(returnObj.flatten())]) return returnObj + # this is generic Stream.measure, also used by Parts def measure(self, measureNumber, + *, collect=('Clef', 'TimeSignature', 'Instrument', 'KeySignature'), indicesNotNumbers=False) -> Measure | None: ''' @@ -4624,7 +4629,6 @@ def measure(self, in that this method returns a single Measure object, not a Stream containing one or more Measure objects. - >>> a = corpus.parse('bach/bwv324.xml') >>> a.parts[0].measure(3) @@ -4659,27 +4663,16 @@ def measure(self, if startMeasureNumber == -1: endMeasureNumber = None - # we must be able to obtain a measure from this (not a flat) - # representation (e.g., this is a Stream or Part, not a Score) - if self.getElementsByClass(Measure): - # environLocal.printDebug(['got measures from getElementsByClass']) - s = self.measures(startMeasureNumber, - endMeasureNumber, - collect=collect, - indicesNotNumbers=indicesNotNumbers) - measureIter = s.getElementsByClass(Measure) - m: Measure | None - # noinspection PyTypeChecker - m = measureIter.first() - if m is None: # not 'if not m' because m might be an empty measure. - return None - else: - self.coreSelfActiveSite(m) - # ^^ this sets its offset to something meaningful... - return m - else: - # environLocal.printDebug(['got not measures from getElementsByClass']) - return None + matchingMeasures = self._getMeasureNumberListByStartEnd( + startMeasureNumber, + endMeasureNumber, + indicesNotNumbers=indicesNotNumbers + ) + if matchingMeasures: + m = matchingMeasures[0] + self.coreSelfActiveSite(m) # not needed? + return m + return None def template(self, *, @@ -5274,7 +5267,10 @@ def _treatAtSoundingPitch(self) -> bool | str: ''' at_sounding = self.atSoundingPitch if self.atSoundingPitch == 'unknown': - for site in self.sites: + for contextTuple in self.contextSites(): + # follow derivations to find one something in a derived hierarchy + # where soundingPitch might be defined. + site = contextTuple.site if site.isStream and site.atSoundingPitch != 'unknown': at_sounding = site.atSoundingPitch break @@ -13710,7 +13706,7 @@ def measures(self, # insert all at zero measuredPart = p.measures(numberStart, numberEnd, - collect, + collect=collect, gatherSpanners=gatherSpanners, indicesNotNumbers=indicesNotNumbers) post.insert(0, measuredPart) @@ -13726,6 +13722,7 @@ def measures(self, post.derivation.method = 'measures' return post + # this is Score.measure def measure(self, measureNumber, collect=(clef.Clef, meter.TimeSignature, instrument.Instrument, key.KeySignature), @@ -13740,7 +13737,6 @@ def measure(self, This method overrides the :meth:`~music21.stream.Stream.measure` method on Stream to allow for finding a single "measure slice" within parts: - >>> bachIn = corpus.parse('bach/bwv324.xml') >>> excerpt = bachIn.measure(2) >>> excerpt @@ -14356,7 +14352,6 @@ class SpannerStorage(Stream): Changed in v8: spannerParent is renamed client. ''' - def __init__(self, givenElements=None, *, client: spanner.Spanner, **keywords): # No longer need store as weakref since Py2.3 and better references self.client = client diff --git a/music21/stream/streamStatus.py b/music21/stream/streamStatus.py index 0bab16ee26..d1b179da57 100644 --- a/music21/stream/streamStatus.py +++ b/music21/stream/streamStatus.py @@ -14,7 +14,7 @@ import unittest from music21 import environment -from music21 import common +from music21.common.misc import defaultDeepcopy from music21.common.objects import SlottedObjectMixin environLocal = environment.Environment(__file__) @@ -57,7 +57,6 @@ class StreamStatus(SlottedObjectMixin): __slots__ = ( '_accidentals', '_beams', - '_client', '_concertPitch', '_dirty', '_enharmonics', @@ -66,12 +65,12 @@ class StreamStatus(SlottedObjectMixin): '_rests', '_ties', '_tuplets', + 'client', ) # INITIALIZER # def __init__(self, client=None): - self._client = None self._accidentals = None self._beams = None self._concertPitch = None @@ -91,24 +90,7 @@ def __deepcopy__(self, memo=None): Manage deepcopying by creating a new reference to the same object. leaving out the client ''' - new = type(self)() - for x in self.__slots__: - if x == '_client': - new._client = None - else: - setattr(new, x, getattr(self, x)) - - return new - - # unwrap weakref for pickling - - def __getstate__(self): - self._client = common.unwrapWeakref(self._client) - return SlottedObjectMixin.__getstate__(self) - - def __setstate__(self, state): - SlottedObjectMixin.__setstate__(self, state) - self._client = common.wrapWeakref(self._client) + return defaultDeepcopy(self, memo, ignoreAttributes={'client'}) # PUBLIC METHODS # @@ -173,15 +155,6 @@ def haveTupletBracketsBeenMade(self): # PUBLIC PROPERTIES # - @property - def client(self): - return common.unwrapWeakref(self._client) - - @client.setter - def client(self, client): - # client is the Stream that this status lives on - self._client = common.wrapWeakref(client) - @property def accidentals(self): if self._accidentals is None: diff --git a/music21/test/test_base.py b/music21/test/test_base.py index eb2710ddd2..efe4c8b831 100644 --- a/music21/test/test_base.py +++ b/music21/test/test_base.py @@ -957,6 +957,7 @@ def testContextSitesA(self): self.assertEqual( siteList, ['(, 0.0, )', + '(, 0.0, )', '(, 9.0, )', '(, 9.0, )'] ) diff --git a/music21/text.py b/music21/text.py index 9869566279..925057c6a5 100644 --- a/music21/text.py +++ b/music21/text.py @@ -372,7 +372,7 @@ def page(self, value): # ------------------------------------------------------------------------------ -_stored_trigrams = {} +_stored_trigrams: dict[str, Trigram] = {} class LanguageDetector: diff --git a/music21/tie.py b/music21/tie.py index d71a87372a..c37d370714 100644 --- a/music21/tie.py +++ b/music21/tie.py @@ -147,11 +147,12 @@ def _reprInternal(self): class Test(unittest.TestCase): - pass + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) # ------------------------------------------------------------------------------ - if __name__ == '__main__': import music21 music21.mainTest(Test) diff --git a/music21/variant.py b/music21/variant.py index 8b5490a4e6..87252823ca 100644 --- a/music21/variant.py +++ b/music21/variant.py @@ -111,25 +111,6 @@ def __init__( if name is not None: self.groups.append(name) - - def _deepcopySubclassable(self, memo=None, ignoreAttributes=None): - ''' - see __deepcopy__ on Spanner for tests and docs - ''' - # NOTE: this is a performance critical operation - defaultIgnoreSet = {'_cache'} - if ignoreAttributes is None: - ignoreAttributes = defaultIgnoreSet - else: - ignoreAttributes = ignoreAttributes | defaultIgnoreSet - - new = super()._deepcopySubclassable(memo, ignoreAttributes) - - return new - - def __deepcopy__(self, memo=None): - return self._deepcopySubclassable(memo) - # -------------------------------------------------------------------------- # as _stream is a private Stream, unwrap/wrap methods need to override # Music21Object to get at these objects @@ -2521,6 +2502,9 @@ def _getPreviousElement(s, v): # ------------------------------------------------------------------------------ class Test(unittest.TestCase): + def testCopyAndDeepcopy(self): + from music21.test.commonTest import testCopyAll + testCopyAll(self, globals()) def pitchOut(self, listIn): out = '[' diff --git a/music21/voiceLeading.py b/music21/voiceLeading.py index 15d5bcfa6a..a392c7dbc4 100644 --- a/music21/voiceLeading.py +++ b/music21/voiceLeading.py @@ -2029,7 +2029,7 @@ class ThreeNoteLinearSegment(NNoteLinearSegment): >>> ex2 = voiceLeading.ThreeNoteLinearSegment('a', 'b', 'c') >>> ex2.n1 - >>> ex2.n1.pitch.defaultOctave + >>> defaults.pitchOctave 4 ''' diff --git a/music21/volume.py b/music21/volume.py index d65eb1acce..6ba4e1ef38 100644 --- a/music21/volume.py +++ b/music21/volume.py @@ -53,10 +53,16 @@ class Volume(prebase.ProtoM21Object, SlottedObjectMixin): >>> v.velocity 90 + + >>> n = note.Note('C5') + >>> v = n.volume + >>> v.velocity = 20 + >>> v.client is n + True ''' # CLASS VARIABLES # __slots__ = ( - '_client', + 'client', '_velocityScalar', '_cachedRealized', 'velocityIsRelative', @@ -64,14 +70,13 @@ class Volume(prebase.ProtoM21Object, SlottedObjectMixin): def __init__( self, - client=None, + client: note.NotRest | None = None, velocity=None, velocityScalar=None, velocityIsRelative=True, ): # store a reference to the client, as we use this to do context # will use property; if None will leave as None - self._client = None self.client = client self._velocityScalar = None if velocity is not None: @@ -84,26 +89,15 @@ def __init__( # SPECIAL METHODS # def __deepcopy__(self, memo=None): ''' - Need to manage copying of weak ref; when copying, do not copy weak ref, - but keep as a reference to the same object. + Don't copy the client; set to current ''' - new = self.__class__() - new.mergeAttributes(self) # will get all numerical values - # keep same weak ref object - new._client = self._client + new = common.defaultDeepcopy(self, memo, ignoreAttributes={'client'}) + new.client = self.client return new def _reprInternal(self): return f'realized={round(self.realized, 2)}' - def __getstate__(self): - self._client = common.unwrapWeakref(self._client) - return SlottedObjectMixin.__getstate__(self) - - def __setstate__(self, state): - SlottedObjectMixin.__setstate__(self, state) - self._client = common.wrapWeakref(self._client) - # PUBLIC METHODS # def getDynamicContext(self): ''' @@ -315,28 +309,6 @@ def cachedRealizedStr(self): ''' return str(round(self.cachedRealized, 2)) - @property - def client(self): - ''' - Get or set the client, which must be a note.NotRest subclass. The - client is wrapped in a weak reference. - ''' - if self._client is None: - return None - post = common.unwrapWeakref(self._client) - if post is None: - # set attribute for speed - self._client = None - return post - - @client.setter - def client(self, client): - if client is not None: - if isinstance(client, note.NotRest): - self._client = common.wrapWeakref(client) - else: - self._client = None - @property def realized(self): return self.getRealized() @@ -514,13 +486,13 @@ def testBasic(self): import gc from music21 import volume - n1 = note.Note() + n1 = note.Note('G#4') v = volume.Volume(client=n1) self.assertEqual(v.client, n1) del n1 gc.collect() - # weak ref does not exist - self.assertEqual(v.client, None) + # Now client is still there -- no longer weakref + self.assertEqual(v.client, note.Note('G#4')) def testGetContextSearchA(self):