From bbab476bb54ad929ce1511b1857c369c5d79ce41 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Sat, 27 Jul 2024 17:02:08 -1000 Subject: [PATCH 1/8] Improve typing on Meter.Core Working on better Meter in music21j, and needed to better understand how meter works in m21p, so added some typing. --- music21/meter/core.py | 193 +++++++++++++++++++++++++++++++++-------- music21/meter/tools.py | 51 ++++++----- 2 files changed, 180 insertions(+), 64 deletions(-) diff --git a/music21/meter/core.py b/music21/meter/core.py index 1b665dd58..76083fdc2 100644 --- a/music21/meter/core.py +++ b/music21/meter/core.py @@ -15,6 +15,7 @@ ''' from __future__ import annotations +from collections.abc import Sequence import copy from music21 import prebase @@ -238,7 +239,10 @@ def subdivideByOther(self, other: 'music21.meter.MeterSequence'): # ms.partitionByOtherMeterSequence(other) # this will split weight return ms - def subdivide(self, value): + def subdivide( + self, + value: Sequence[int | string] | MeterSequence | int + ): ''' Subdivision takes a MeterTerminal and, making it into a collection of MeterTerminals, Returns a MeterSequence. @@ -397,7 +401,7 @@ def __init__(self, value=None, partitionRequest=None): self._numerator = None # rationalized self._denominator = None # lowest common multiple - self._partition = [] # a list of terminals or MeterSequences + self._partition: list[MeterTerminal] = [] # a list of terminals or MeterSequences self._overriddenDuration = None self._levelListCache = {} @@ -487,7 +491,7 @@ def __len__(self): ''' return len(self._partition) - def __setitem__(self, key, value): + def __setitem__(self, key: int, value: MeterTerminal): ''' Insert items at index positions. @@ -549,7 +553,7 @@ def partitionDisplay(self): # ------------------------------------------------------------------------- - def _clearPartition(self): + def _clearPartition(self) -> None: ''' This will not sync with .numerator and .denominator if called alone ''' @@ -557,7 +561,7 @@ def _clearPartition(self): # clear cache self._levelListCache = {} - def _addTerminal(self, value): + def _addTerminal(self, value: MeterTerminal|str) -> None: ''' Add an object to the partition list. This does not update numerator and denominator. @@ -619,7 +623,7 @@ def getPartitionOptions(self) -> tools.MeterOptions: # ------------------------------------------------------------------------- - def partitionByCount(self, countRequest, loadDefault=True): + def partitionByCount(self, countRequest: int, loadDefault: bool = True) -> None: ''' Divide the current MeterSequence into the requested number of parts. @@ -683,13 +687,11 @@ def partitionByCount(self, countRequest, loadDefault=True): # if no matches this method provides a default if optMatch is None: - if loadDefault: + if loadDefault and opts: optMatch = opts[0] else: - numerator = self.numerator - denom = self.denominator raise MeterException( - f'Cannot set partition by {countRequest} ({numerator}/{denom})' + f'Cannot set partition by {countRequest} ({self.numerator}/{self.denominator})' ) targetWeight = self.weight @@ -702,7 +704,7 @@ def partitionByCount(self, countRequest, loadDefault=True): # clear cache self._levelListCache = {} - def partitionByList(self, numeratorList): + def partitionByList(self, numeratorList: Sequence[int | str]) -> None: ''' Given a numerator list, partition MeterSequence into a new list of MeterTerminals @@ -741,6 +743,7 @@ def partitionByList(self, numeratorList): # assume a list of terminal definitions if isinstance(numeratorList[0], str): + # TODO: working with private methods of a created MeterSequence test = MeterSequence() for mtStr in numeratorList: test._addTerminal(mtStr) @@ -762,7 +765,6 @@ def partitionByList(self, numeratorList): # last resort: search options else: opts = self.getPartitionOptions() - optMatch = None for opt in opts: # get numerators as numbers nFound = [int(x.split('/')[0]) for x in opt] @@ -785,11 +787,10 @@ def partitionByList(self, numeratorList): # clear cache self._levelListCache = {} - def partitionByOtherMeterSequence(self, other): + def partitionByOtherMeterSequence(self, other: MeterSequence) -> None: ''' Set partition to that found in another - MeterSequence - + MeterSequence. >>> a = meter.MeterSequence('4/4', 4) >>> str(a) @@ -815,7 +816,11 @@ def partitionByOtherMeterSequence(self, other): # clear cache self._levelListCache = {} - def partition(self, value, loadDefault=False): + def partition( + self, + value: int | Sequence[string | MeterTerminal] | MeterSequence, + loadDefault=False + ) -> None: ''' Partitioning creates and sets a number of MeterTerminals that make up this MeterSequence. @@ -870,7 +875,7 @@ def partition(self, value, loadDefault=False): else: raise MeterException(f'cannot process partition argument {value}') - def subdividePartitionsEqual(self, divisions=None): + def subdividePartitionsEqual(self, divisions: int|None = None) -> None: ''' Subdivide all partitions by equally-spaced divisions, given a divisions value. Manipulates this MeterSequence in place. @@ -878,13 +883,53 @@ def subdividePartitionsEqual(self, divisions=None): Divisions value may optionally be a MeterSequence, from which a top-level partitioning structure is derived. + Example: First we will do a normal partition (not subdivided partition) + >>> ms = meter.MeterSequence('2/4') + >>> ms + + >>> len(ms) + 1 + >>> ms[0] + + >>> len(ms[0]) + Traceback (most recent call last): + TypeError: object of type 'MeterTerminal' has no len() + + + Divide the Sequence into two parts, so now there are two + MeterTerminals of 1/4 each: + >>> ms.partition(2) >>> ms + >>> len(ms) + 2 + >>> ms[0] + + >>> ms[1] + + + But what happens if we want to divide each of those into 1/8+1/8 are replace + them by MeterSequences? subdividePartitionsEqual is what is needed. + >>> ms.subdividePartitionsEqual(2) >>> ms + + Length is still 2, but each of the components are now MeterSequences of their + own: + + >>> len(ms) + 2 + >>> ms[0] + + >>> ms[1] + + + There is not a way (the authors know of...) to get to the next level. + You would just need to do them individually. + >>> ms[0].subdividePartitionsEqual(2) >>> ms @@ -892,21 +937,36 @@ def subdividePartitionsEqual(self, divisions=None): >>> ms + If None is given as a parameter, then it will try to find something logical. + >>> ms = meter.MeterSequence('2/4+3/4') >>> ms.subdividePartitionsEqual(None) >>> ms + + If any partition cannot be divided by the given count, a MeterException is raised: + + >>> ms = meter.MeterSequence('5/8+3/8') + >>> len(ms) + 2 + >>> ms.subdividePartitionsEqual(5) + Traceback (most recent call last): + music21.exceptions21.MeterException: Cannot set partition by 5 (3/8) + ''' + divisionsLocal: int = 1 for i in range(len(self)): if divisions is None: # get dynamically - if self[i].numerator in [1, 2, 4, 8, 16]: + partitionNumerator: int = self[i].numerator + if partitionNumerator in (1, 2, 4, 8, 16, 32, 64): divisionsLocal = 2 - elif self[i].numerator in [3]: + elif partitionNumerator == 3: divisionsLocal = 3 - elif self[i].numerator in [6, 9, 12, 15, 18]: - divisionsLocal = self[i].numerator / 3 + elif partitionNumerator in (6, 9, 12, 15, 18, 21, 24, 27): + divisionsLocal = partitionNumerator // 3 else: - divisionsLocal = self[i].numerator + # TODO: get from the smallest primer number... + divisionsLocal = partitionNumerator else: divisionsLocal = divisions # environLocal.printDebug(['got divisions:', divisionsLocal, @@ -919,18 +979,42 @@ def subdividePartitionsEqual(self, divisions=None): def _subdivideNested(self, processObjList, divisions): # noinspection PyShadowingNames ''' - Recursive nested call routine. Return a reference to the newly created level. + Recursive nested call routine. Returns a list of the MeterSequences at the newly created + level. >>> ms = meter.MeterSequence('2/4') >>> ms.partition(2) >>> ms + >>> ms[0] + + >>> post = ms._subdivideNested([ms], 2) >>> ms - >>> post = ms._subdivideNested(post, 2) # pass post here + >>> ms[0] + + >>> post + [, ] + >>> ms[0] is post[0] + True + + >>> post2 = ms._subdivideNested(post, 2) # pass post here >>> ms + >>> post2 + [, + , + , + ] + + Notice that since we gave a list of lists, post2 is now one level down + + >>> post2[0] is ms[0] + False + >>> post2[0] is ms[0][0] + True + ''' for obj in processObjList: obj.subdividePartitionsEqual(divisions) @@ -1102,9 +1186,9 @@ def partitionStr(self): # loading is always destructive def load(self, - value, - partitionRequest=None, - autoWeight=False, + value: str | MeterTerminal | Sequence[MeterTerminal | str], + partitionRequest: int | Sequence[string | MeterTerminal] | MeterSequence | None = None, + autoWeight: boolean = False, targetWeight=None): ''' This method is called when a MeterSequence is created, or if a MeterSequence is re-set. @@ -1201,32 +1285,65 @@ def _updateRatio(self): # do not permit setting of numerator/denominator @property - def weight(self): + def weight(self) -> int | float: ''' - Get or set the weight for each object in this MeterSequence + Get the weight for the MeterSequence, or set the weight and thereby change the weights + for each object in this MeterSequence. + + By default, all the partitions of a MeterSequence's weights sum to 1.0 >>> a = meter.MeterSequence('3/4') + >>> a.weight + 1.0 >>> a.partition(3) - >>> a.weight = 1 + >>> a + + >>> a.weight + 1.0 >>> a[0].weight - 0.333... - >>> b = meter.MeterTerminal('1/4', 0.25) - >>> c = meter.MeterTerminal('1/4', 0.25) - >>> d = meter.MeterSequence([b, c]) - >>> d.weight - 0.5 + 0.3333... + + But this MeterSequence might be embedded in another one, so perhaps + its weight should be 0.5? + + >>> a.weight = 0.5 + >>> a[0].weight + 0.16666... + + When creating a new MeterSequence from MeterTerminals, the sequence has + the weight of the sum of those creating it. + + >>> downbeat = meter.MeterTerminal('1/4', 0.5) + >>> upbeat = meter.MeterTerminal('1/4', 0.25) + >>> accentSequence = meter.MeterSequence([downbeat, upbeat]) + >>> accentSequence.weight + 0.75 + + Changing the weight of the child sequence will affect the parent, since this is + not cached, but recomputed on each call. + + >>> downbeat.weight = 0.375 + >>> accentSequence.weight + 0.625 + + Changing the weight on the parent sequence will reset weights on the children + + >>> accentSequence.weight = 1.0 + >>> (downbeat.weight, upbeat.weight) + (0.5, 0.5) + Assume this MeterSequence is a whole, not a part of some larger MeterSequence. Thus, we cannot use numerator/denominator relationship as a scalar. ''' - summation = 0 + summation = 0.0 for obj in self._partition: summation += obj.weight # may be a MeterTerminal or MeterSequence return summation @weight.setter - def weight(self, value): + def weight(self, value: int | float | None) -> None: # environLocal.printDebug(['calling setWeight with value', value]) if value is None: diff --git a/music21/meter/tools.py b/music21/meter/tools.py index 924674a25..effd4d9c5 100644 --- a/music21/meter/tools.py +++ b/music21/meter/tools.py @@ -30,8 +30,8 @@ NumDenomTuple = tuple[NumDenom, ...] MeterOptions = tuple[tuple[str, ...], ...] -validDenominators = [1, 2, 4, 8, 16, 32, 64, 128] # in order -validDenominatorsSet = set(validDenominators) +validDenominators = (1, 2, 4, 8, 16, 32, 64, 128) # in order +validDenominatorsSet = frozenset(validDenominators) @lru_cache(512) @@ -51,13 +51,16 @@ def slashToTuple(value: str) -> MeterTerminalTuple: valueNumbers, valueChars = common.getNumFromStr(value, numbers='0123456789/.') valueNumbers = valueNumbers.strip() # remove whitespace - valueChars = valueChars.strip() # remove whitespace - if 'slow' in valueChars.lower(): - division = MeterDivision.SLOW - elif 'fast' in valueChars.lower(): - division = MeterDivision.FAST + if not valueChars: + division = MeterDivision.NONE # speed up most common case else: - division = MeterDivision.NONE + valueChars = valueChars.strip() # remove whitespace + if 'slow' in valueChars.lower(): + division = MeterDivision.SLOW + elif 'fast' in valueChars.lower(): + division = MeterDivision.FAST + else: + division = MeterDivision.NONE matches = re.match(r'(\d+)/(\d+)', valueNumbers) if matches is not None: @@ -134,8 +137,7 @@ def slashMixedToFraction(valueSrc: str) -> tuple[NumDenomTuple, bool]: ''' pre: list[NumDenom|tuple[int, None]] = [] summedNumerator = False - value = valueSrc.strip() # rem whitespace - value = value.split('+') + value = valueSrc.strip().split('+') for part in value: if '/' in part: try: @@ -258,7 +260,7 @@ def fractionSum(numDenomTuple: NumDenomTuple) -> NumDenom: def proportionToFraction(value: float) -> NumDenom: ''' Given a floating point proportional value between 0 and 1, return the - best-fit slash-base fraction + best-fit slash-base fraction up to 16. >>> from music21.meter.tools import proportionToFraction >>> proportionToFraction(0.5) @@ -286,7 +288,7 @@ def proportionToFraction(value: float) -> NumDenom: # load common meter templates into this sequence # no need to cache these -- getPartitionOptions is cached -def divisionOptionsFractionsUpward(n, d) -> tuple[str, ...]: +def divisionOptionsFractionsUpward(n: int, d: int) -> tuple[str, ...]: ''' This simply gets restatements of the same fraction in smaller units, up to the largest valid denominator. @@ -310,7 +312,7 @@ def divisionOptionsFractionsUpward(n, d) -> tuple[str, ...]: return tuple(opts) -def divisionOptionsFractionsDownward(n, d) -> tuple[str, ...]: +def divisionOptionsFractionsDownward(n: int, d: int) -> tuple[str, ...]: ''' Get restatements of the same fraction in larger units @@ -334,7 +336,7 @@ def divisionOptionsFractionsDownward(n, d) -> tuple[str, ...]: return tuple(opts) -def divisionOptionsAdditiveMultiplesDownward(n, d) -> MeterOptions: +def divisionOptionsAdditiveMultiplesDownward(n: int, d: int) -> MeterOptions: ''' >>> meter.tools.divisionOptionsAdditiveMultiplesDownward(1, 16) (('1/32', '1/32'), ('1/64', '1/64', '1/64', '1/64'), @@ -352,7 +354,7 @@ def divisionOptionsAdditiveMultiplesDownward(n, d) -> MeterOptions: return tuple(opts) -def divisionOptionsAdditiveMultiples(n, d) -> MeterOptions: +def divisionOptionsAdditiveMultiples(n: int, d: int) -> MeterOptions: ''' Additive multiples with the same denominators. @@ -375,7 +377,7 @@ def divisionOptionsAdditiveMultiples(n, d) -> MeterOptions: return tuple(opts) -def divisionOptionsAdditiveMultiplesEvenDivision(n, d): +def divisionOptionsAdditiveMultiplesEvenDivision(n: int, d: int) -> tuple[tuple[str, ...], ...]: ''' >>> meter.tools.divisionOptionsAdditiveMultiplesEvenDivision(4, 16) (('1/8', '1/8'),) @@ -399,7 +401,7 @@ def divisionOptionsAdditiveMultiplesEvenDivision(n, d): return tuple(opts) -def divisionOptionsAdditiveMultiplesUpward(n, d) -> MeterOptions: +def divisionOptionsAdditiveMultiplesUpward(n: int, d: int) -> MeterOptions: ''' >>> meter.tools.divisionOptionsAdditiveMultiplesUpward(4, 16) (('1/16', '1/16', '1/16', '1/16'), @@ -421,13 +423,9 @@ def divisionOptionsAdditiveMultiplesUpward(n, d) -> MeterOptions: else: nCountLimit = 16 - while True: - # place practical limits on number of units to get - if dCurrent > validDenominators[-1] or nCount > nCountLimit: - break - seq = [] - for j in range(nCount): - seq.append(f'{1}/{dCurrent}') + # place practical limits on number of units to get + while dCurrent <= validDenominators[-1] and nCount <= nCountLimit: + seq = [f'1/{dCurrent}'] * nCount opts.append(tuple(seq)) # double count, double denominator dCurrent *= 2 @@ -539,14 +537,15 @@ def divisionOptionsAlgo(n, d) -> MeterOptions: opts = [] group: tuple[int, ...] + # TODO: look at music21j code for more readable version. + # compound meters; 6, 9, 12, 15, 18 # 9/4, 9/2, 6/2 are all considered compound without d>4 # if n % 3 == 0 and n > 3 and d > 4: if n % 3 == 0 and n > 3: - nMod = n / 3 seq = [] for j in range(int(n / 3)): - seq.append(f'{3}/{d}') + seq.append(f'3/{d}') opts.append(tuple(seq)) # odd meters with common groupings if n == 5: From ea0217292aa70ccd40150fc7d7578b5a0fcb32be Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Sat, 27 Jul 2024 17:17:02 -1000 Subject: [PATCH 2/8] Pin numpy to <2.0.0 for now --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d0e75c050..8ec736d1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ joblib jsonpickle matplotlib more_itertools -numpy +numpy<2.0.0 webcolors>=1.5 requests From e93ebc4e608e4a5e0db7f0e0c1abbc20b3bd2f2f Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Sat, 27 Jul 2024 22:58:41 -1000 Subject: [PATCH 3/8] fix new problems --- music21/meter/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/music21/meter/core.py b/music21/meter/core.py index 76083fdc2..c61aa0e7e 100644 --- a/music21/meter/core.py +++ b/music21/meter/core.py @@ -241,7 +241,7 @@ def subdivideByOther(self, other: 'music21.meter.MeterSequence'): def subdivide( self, - value: Sequence[int | string] | MeterSequence | int + value: Sequence[int | str] | MeterSequence | int ): ''' Subdivision takes a MeterTerminal and, making it into a collection of MeterTerminals, @@ -818,7 +818,7 @@ def partitionByOtherMeterSequence(self, other: MeterSequence) -> None: def partition( self, - value: int | Sequence[string | MeterTerminal] | MeterSequence, + value: int | Sequence[str | MeterTerminal] | MeterSequence, loadDefault=False ) -> None: ''' @@ -1187,8 +1187,8 @@ def partitionStr(self): def load(self, value: str | MeterTerminal | Sequence[MeterTerminal | str], - partitionRequest: int | Sequence[string | MeterTerminal] | MeterSequence | None = None, - autoWeight: boolean = False, + partitionRequest: int | Sequence[str | MeterTerminal] | MeterSequence | None = None, + autoWeight: bool = False, targetWeight=None): ''' This method is called when a MeterSequence is created, or if a MeterSequence is re-set. From 1b4146b25deb5cde2860f8c71bce1b77813445f6 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 17 Sep 2024 01:02:29 -1000 Subject: [PATCH 4/8] little better work --- music21/meter/core.py | 48 ++++++++++++++++++++++-------------------- music21/meter/tools.py | 2 +- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/music21/meter/core.py b/music21/meter/core.py index c61aa0e7e..188f0777e 100644 --- a/music21/meter/core.py +++ b/music21/meter/core.py @@ -17,6 +17,7 @@ from collections.abc import Sequence import copy +import typing as t from music21 import prebase from music21.common.numberTools import opFrac @@ -324,23 +325,19 @@ def _ratioChanged(self): ''' # NOTE: this is a performance critical method and should only be # called when necessary - if self.numerator is None or self.denominator is None: - self._duration = None - else: - self._duration = duration.Duration() - try: - self._duration.quarterLength = ( - (4.0 * self.numerator) / self.denominator - ) - except duration.DurationException: - environLocal.printDebug( - ['DurationException encountered', - 'numerator/denominator', - self.numerator, - self.denominator - ] - ) - self._duration = None + self._duration = duration.Duration() + try: + self._duration.quarterLength = ( + (4.0 * self.numerator) / self.denominator + ) + except duration.DurationException: + environLocal.printDebug( + ['DurationException encountered', + 'numerator/denominator', + self.numerator, + self.denominator + ] + ) def _getDuration(self): ''' @@ -396,11 +393,15 @@ class MeterSequence(MeterTerminal): # INITIALIZER # - def __init__(self, value=None, partitionRequest=None): + def __init__( + self, + value: str | MeterTerminal | Sequence[MeterTerminal | str] | None = None, + partitionRequest: t.Any|None = None, + ): super().__init__() - self._numerator = None # rationalized - self._denominator = None # lowest common multiple + self._numerator: int = 1 # rationalized + self._denominator: int = 0 # lowest common multiple self._partition: list[MeterTerminal] = [] # a list of terminals or MeterSequences self._overriddenDuration = None self._levelListCache = {} @@ -432,7 +433,6 @@ def __deepcopy__(self, memo=None): Notably, self._levelListCache is not copied, which may not be needed in the copy and may be large. - >>> from copy import deepcopy >>> ms1 = meter.MeterSequence('4/4+3/8') >>> ms2 = deepcopy(ms1) @@ -745,6 +745,8 @@ def partitionByList(self, numeratorList: Sequence[int | str]) -> None: if isinstance(numeratorList[0], str): # TODO: working with private methods of a created MeterSequence test = MeterSequence() + if t.TYPE_CHECKING: + numeratorList = cast(list[str], numeratorList) for mtStr in numeratorList: test._addTerminal(mtStr) test._updateRatio() @@ -754,9 +756,9 @@ def partitionByList(self, numeratorList: Sequence[int | str]) -> None: else: raise MeterException(f'Cannot set partition by {numeratorList}') - elif sum(numeratorList) in [self.numerator * x for x in range(1, 9)]: + elif sum(t.cast(list[int], numeratorList)) in [self.numerator * x for x in range(1, 9)]: for i in range(1, 9): - if sum(numeratorList) == self.numerator * i: + if sum(t.cast(list[int], numeratorList)) == self.numerator * i: optMatch = [] for n in numeratorList: optMatch.append(f'{n}/{self.denominator * i}') diff --git a/music21/meter/tools.py b/music21/meter/tools.py index effd4d9c5..a134f56fd 100644 --- a/music21/meter/tools.py +++ b/music21/meter/tools.py @@ -52,7 +52,7 @@ def slashToTuple(value: str) -> MeterTerminalTuple: numbers='0123456789/.') valueNumbers = valueNumbers.strip() # remove whitespace if not valueChars: - division = MeterDivision.NONE # speed up most common case + division = MeterDivision.NONE # speed up most common case else: valueChars = valueChars.strip() # remove whitespace if 'slow' in valueChars.lower(): From 65b898bb0fd5b465348212ecb0febd6d48d2dbd9 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 17 Sep 2024 15:01:59 -1000 Subject: [PATCH 5/8] More typing; opFrac(None) is gone --- music21/common/numberTools.py | 22 +++++----- music21/meter/base.py | 43 ++++++++++++-------- music21/meter/core.py | 76 +++++++++++++++++++++-------------- music21/meter/tools.py | 13 ++++-- 4 files changed, 92 insertions(+), 62 deletions(-) diff --git a/music21/common/numberTools.py b/music21/common/numberTools.py index 924f242af..7dd2688b4 100644 --- a/music21/common/numberTools.py +++ b/music21/common/numberTools.py @@ -236,10 +236,6 @@ def _preFracLimitDenominator(n: int, d: int) -> tuple[int, int]: 0.25, 0.375, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0 ]) -@overload -def opFrac(num: None) -> None: - pass - @overload def opFrac(num: int) -> float: pass @@ -249,13 +245,13 @@ def opFrac(num: float|Fraction) -> float|Fraction: pass # no type checking due to accessing protected attributes (for speed) -def opFrac(num: OffsetQLIn|None) -> OffsetQL|None: +def opFrac(num: OffsetQLIn) -> OffsetQL: ''' opFrac -> optionally convert a number to a fraction or back. Important music21 function for working with offsets and quarterLengths - Takes in a number (or None) and converts it to a Fraction with denominator + Takes in a number and converts it to a Fraction with denominator less than limitDenominator if it is not binary expressible; otherwise return a float. Or if the Fraction can be converted back to a binary expressible float then do so. @@ -290,8 +286,14 @@ def opFrac(num: OffsetQLIn|None) -> OffsetQL|None: Fraction(10, 81) >>> common.opFrac(0.000001) 0.0 - >>> common.opFrac(None) is None - True + + Please check against None before calling, but None is changed to 0.0 + + >>> common.opFrac(None) + 0.0 + + * Changed in v9.3: opFrac(None) should not be called. If it is called, + it now returns 0.0 ''' # This is a performance critical operation, tuned to go as fast as possible. # hence redundancy -- first we check for type (no inheritance) and then we @@ -340,8 +342,8 @@ def opFrac(num: OffsetQLIn|None) -> OffsetQL|None: return num._numerator / (d + 0.0) # type: ignore else: return num # leave non-power of two fractions alone - elif num is None: - return None + elif num is None: # undocumented -- used to be documented to return None. callers must check. + return 0.0 # class inheritance only check AFTER "type is" checks... this is redundant but highly optimized. elif isinstance(num, int): diff --git a/music21/meter/base.py b/music21/meter/base.py index 34322e436..e8dcdcf4a 100644 --- a/music21/meter/base.py +++ b/music21/meter/base.py @@ -265,6 +265,7 @@ class TimeSignatureBase(base.Music21Object): pass class TimeSignature(TimeSignatureBase): + # noinspection GrazieInspection r''' The `TimeSignature` object represents time signatures in musical scores (4/4, 3/8, 2/4+5/16, Cut, etc.). @@ -282,7 +283,7 @@ class TimeSignature(TimeSignatureBase): >>> ts = meter.TimeSignature('3/4') >>> m1.insert(0, ts) >>> m1.insert(0, note.Note('C#3', type='half')) - >>> n = note.Note('D3', type='quarter') # we will need this later + >>> n = note.Note('D3', type='quarter') >>> m1.insert(1.0, n) >>> m1.number = 1 >>> p.insert(0, m1) @@ -912,7 +913,7 @@ def beatDuration(self) -> duration.Duration: Return a :class:`~music21.duration.Duration` object equal to the beat unit of this Time Signature, if and only if this TimeSignature has a uniform beat unit. - Otherwise raises an exception in v7.1 but will change to returning NaN + Otherwise, raises an exception in v7.1 but will change to returning NaN soon fasterwards. >>> ts = meter.TimeSignature('3/4') @@ -1260,8 +1261,8 @@ def _setDefaultAccentWeights(self, depth: int = 3) -> None: firstPartitionForm = self.beatSequence cacheKey = _meterSequenceAccentArchetypesNoneCache # cannot cache based on beat form - # environLocal.printDebug(['_setDefaultAccentWeights(): firstPartitionForm set to', - # firstPartitionForm, 'self.beatSequence: ', self.beatSequence, tsStr]) + # environLocal.printDebug('_setDefaultAccentWeights(): firstPartitionForm set to', + # firstPartitionForm, 'self.beatSequence: ', self.beatSequence, tsStr) # using cacheKey speeds up TS creation from 2300 microseconds to 500microseconds try: self.accentSequence = copy.deepcopy( @@ -1421,7 +1422,7 @@ def fixBeamsOneElementDepth(i, el, depth): start = opFrac(pos) end = opFrac(pos + dur.quarterLength) - startNext = end + startNext: float|Fraction = end isLast = (i == len(srcList) - 1) isFirst = (i == 0) @@ -1903,27 +1904,33 @@ def getOffsetFromBeat(self, beat): >>> ts1.getOffsetFromBeat(3.25) 2.25 + Get the offset from beat 8/3 (2.6666): give a Fraction, get a Fraction. + >>> from fractions import Fraction - >>> ts1.getOffsetFromBeat(Fraction(8, 3)) # 2.66666 + >>> ts1.getOffsetFromBeat(Fraction(8, 3)) Fraction(5, 3) - >>> ts1 = meter.TimeSignature('6/8') >>> ts1.getOffsetFromBeat(1) 0.0 >>> ts1.getOffsetFromBeat(2) 1.5 + + Check that 2.5 is 2.5 + (0.5 * 1.5): + + >>> ts1.getOffsetFromBeat(2.5) + 2.25 + + Decimals only need to be pretty close to work. + (But Fractions are better as demonstrated above) + >>> ts1.getOffsetFromBeat(2.33) 2.0 - >>> ts1.getOffsetFromBeat(2.5) # will be + 0.5 * 1.5 - 2.25 >>> ts1.getOffsetFromBeat(2.66) 2.5 - Works for asymmetrical meters as well: - >>> ts3 = meter.TimeSignature('3/8+2/8') # will partition as 2 beat >>> ts3.getOffsetFromBeat(1) 0.0 @@ -1936,16 +1943,18 @@ def getOffsetFromBeat(self, beat): Let's try this on a real piece, a 4/4 chorale with a one beat pickup. Here we get the - normal offset from the active TimeSignature, but we subtract out the pickup length which - is in a `Measure`'s :attr:`~music21.stream.Measure.paddingLeft` property. + normal offset for beat 4 from the active TimeSignature, but we subtract out + the pickup length which is in a `Measure`'s :attr:`~music21.stream.Measure.paddingLeft` + property, and thus see the distance from the beginning of the measure to beat 4 in + quarter notes >>> c = corpus.parse('bwv1.6') >>> for m in c.parts.first().getElementsByClass(stream.Measure): ... ts = m.timeSignature or m.getContextByClass(meter.TimeSignature) - ... print('%s %s' % (m.number, ts.getOffsetFromBeat(4.5) - m.paddingLeft)) - 0 0.5 - 1 3.5 - 2 3.5 + ... print(m.number, ts.getOffsetFromBeat(4.0) - m.paddingLeft) + 0 0.0 + 1 3.0 + 2 3.0 ... ''' # divide into integer and floating point components diff --git a/music21/meter/core.py b/music21/meter/core.py index 188f0777e..80f9eec15 100644 --- a/music21/meter/core.py +++ b/music21/meter/core.py @@ -23,7 +23,7 @@ from music21.common.numberTools import opFrac from music21.common.objects import SlottedObjectMixin from music21 import common -from music21 import duration +from music21.duration import Duration, DurationException from music21 import environment from music21.exceptions21 import MeterException from music21.meter import tools @@ -58,13 +58,13 @@ class MeterTerminal(prebase.ProtoM21Object, SlottedObjectMixin): ) # INITIALIZER # - def __init__(self, slashNotation: str|None = None, weight=1): + def __init__(self, slashNotation: str|None = None, weight: int = 1): # because of how they are copied, MeterTerminals must not have any # initialization parameters without defaults - self._duration = None - self._numerator = 0 - self._denominator = 1 - self._weight = None + self._duration: Duration|None = None + self._numerator: int = 0 + self._denominator: int = 1 + self._weight: int = 1 self._overriddenDuration = None if slashNotation is not None: @@ -281,56 +281,67 @@ def weight(self): def weight(self, value): self._weight = value - def _getNumerator(self): - return self._numerator - - def _setNumerator(self, value): + @property + def numerator(self) -> int: ''' + Return or set the numerator of the MeterTerminal + >>> a = meter.MeterTerminal('2/4') + >>> a.numerator + 2 >>> a.duration.quarterLength 2.0 >>> a.numerator = 11 >>> a.duration.quarterLength 11.0 ''' + return self._numerator + + @numerator.setter + def numerator(self, value: int): self._numerator = value self._ratioChanged() - numerator = property(_getNumerator, _setNumerator) - - def _getDenominator(self): - return self._denominator - - def _setDenominator(self, value): + @property + def denominator(self) -> int: ''' + Get or set the denominator of the meter terminal >>> a = meter.MeterTerminal('2/4') + >>> a.denominator + 4 >>> a.duration.quarterLength 2.0 - >>> a.numerator = 11 + >>> a.denominator = 8 >>> a.duration.quarterLength - 11.0 + 1.0 + + >>> a.denominator = 7 + Traceback (most recent call last): + music21.exceptions21.MeterException: bad denominator value: 7 ''' + return self._denominator + + @denominator.setter + def denominator(self, value: int): # use duration.typeFromNumDict? if value not in tools.validDenominatorsSet: raise MeterException(f'bad denominator value: {value}') self._denominator = value self._ratioChanged() - denominator = property(_getDenominator, _setDenominator) - def _ratioChanged(self): ''' If ratio has been changed, call this to update duration ''' # NOTE: this is a performance critical method and should only be # called when necessary - self._duration = duration.Duration() + self._duration = Duration() try: self._duration.quarterLength = ( (4.0 * self.numerator) / self.denominator ) - except duration.DurationException: + except DurationException: environLocal.printDebug( ['DurationException encountered', 'numerator/denominator', @@ -339,7 +350,9 @@ def _ratioChanged(self): ] ) - def _getDuration(self): + + @property + def duration(self): ''' duration gets or sets a duration value that is equal in length of the terminal. @@ -361,11 +374,10 @@ def _getDuration(self): else: return self._duration - def _setDuration(self, value): + @duration.setter + def duration(self, value: Duration): self._overriddenDuration = value - duration = property(_getDuration, _setDuration) - @property def depth(self): ''' @@ -704,7 +716,7 @@ def partitionByCount(self, countRequest: int, loadDefault: bool = True) -> None: # clear cache self._levelListCache = {} - def partitionByList(self, numeratorList: Sequence[int | str]) -> None: + def partitionByList(self, numeratorList: Sequence[int] | Sequence[str]) -> None: ''' Given a numerator list, partition MeterSequence into a new list of MeterTerminals @@ -820,7 +832,7 @@ def partitionByOtherMeterSequence(self, other: MeterSequence) -> None: def partition( self, - value: int | Sequence[str | MeterTerminal] | MeterSequence, + value: int | Sequence[str] | Sequence[MeterTerminal] | Sequence[int] | MeterSequence, loadDefault=False ) -> None: ''' @@ -867,6 +879,9 @@ def partition( 3 >>> str(c) '{1/128+1/128+1/128}' + + * Changed in v9.3: if given a list it must either be all numbers, all strings, + or all MeterTerminals, not a mix (which was undocumented and buggy) ''' if common.isListLike(value): self.partitionByList(value) @@ -898,7 +913,6 @@ def subdividePartitionsEqual(self, divisions: int|None = None) -> None: Traceback (most recent call last): TypeError: object of type 'MeterTerminal' has no len() - Divide the Sequence into two parts, so now there are two MeterTerminals of 1/4 each: @@ -1564,7 +1578,7 @@ def getLevelList(self, levelCount, flat=True): if not isinstance(self._partition[i], MeterSequence): mt = self[i] # a meter terminal mtList.append(mt) - else: # its a sequence + else: # it is a sequence if levelCount > 0: # retain this sequence but get lower level # reduce level by 1 when recursing; do not # change levelCount here @@ -1577,7 +1591,7 @@ def getLevelList(self, levelCount, flat=True): # set weight to that of the sequence mt.weight = self._partition[i].weight mtList.append(mt) - else: # its not a terminal, its a meter sequence + else: # it is not a terminal, it is a meter sequence mtList.append(self._partition[i]) # store in cache self._levelListCache[cacheKey] = mtList diff --git a/music21/meter/tools.py b/music21/meter/tools.py index a134f56fd..b77a72e0c 100644 --- a/music21/meter/tools.py +++ b/music21/meter/tools.py @@ -11,11 +11,11 @@ # ----------------------------------------------------------------------------- from __future__ import annotations -import collections import fractions from functools import lru_cache import math import re +import typing as t from music21 import common from music21.common.enums import MeterDivision @@ -24,8 +24,11 @@ environLocal = environment.Environment('meter.tools') -MeterTerminalTuple = collections.namedtuple('MeterTerminalTuple', - ['numerator', 'denominator', 'division']) +class MeterTerminalTuple(t.NamedTuple): + numerator: int + denominator: int + division: MeterDivision + NumDenom = tuple[int, int] NumDenomTuple = tuple[NumDenom, ...] MeterOptions = tuple[tuple[str, ...], ...] @@ -97,6 +100,8 @@ def slashCompoundToFraction(value: str) -> NumDenomTuple: return tuple(post) +# Pycharm is having trouble with the unmatched parentheses in the docs +# noinspection GrazieInspection @lru_cache(512) def slashMixedToFraction(valueSrc: str) -> tuple[NumDenomTuple, bool]: ''' @@ -146,7 +151,7 @@ def slashMixedToFraction(valueSrc: str) -> tuple[NumDenomTuple, bool]: raise TimeSignatureException( f'Cannot create time signature from "{valueSrc}"') from me pre.append((tup.numerator, tup.denominator)) - else: # its just a numerator + else: # it is just a numerator try: pre.append((int(part), None)) summedNumerator = True From fcb49fded4452ffb53d358e33a1b060d3ce21ba5 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 17 Sep 2024 15:25:53 -1000 Subject: [PATCH 6/8] lint/type --- music21/meter/base.py | 17 +++++++++++++--- music21/meter/core.py | 46 ++++++++++++++++++------------------------ music21/meter/tools.py | 1 + 3 files changed, 35 insertions(+), 29 deletions(-) diff --git a/music21/meter/base.py b/music21/meter/base.py index e8dcdcf4a..1f5eee29f 100644 --- a/music21/meter/base.py +++ b/music21/meter/base.py @@ -1306,7 +1306,11 @@ def _setDefaultAccentWeights(self, depth: int = 3) -> None: # -------------------------------------------------------------------------- # access data for other processing - def getBeams(self, srcList, measureStartOffset=0.0) -> list[beam.Beams|None]: + def getBeams( + self, + srcList: stream.Stream|t.Sequence[base.Music21Object], + measureStartOffset: OffsetQL = 0.0, + ) -> list[beam.Beams|None]: ''' Given a qLen position and an iterable of Music21Objects, return a list of Beams objects. @@ -1406,11 +1410,18 @@ def getBeams(self, srcList, measureStartOffset=0.0) -> list[beam.Beams|None]: beamsList = beam.Beams.naiveBeams(srcList) # hold maximum Beams objects, all with type None beamsList = beam.Beams.removeSandwichedUnbeamables(beamsList) - def fixBeamsOneElementDepth(i, el, depth): + def fixBeamsOneElementDepth(i: int, el: base.Music21Object, depth: int): + ''' + Note that this can compute the beams for non-Note things like rests + they just cannot be applied to the object. + ''' beams = beamsList[i] if beams is None: return + if t.TYPE_CHECKING: + assert isinstance(beams, beam.Beams) + beamNumber = depth + 1 # see if there is a component defined for this beam number # if not, continue @@ -1422,7 +1433,7 @@ def fixBeamsOneElementDepth(i, el, depth): start = opFrac(pos) end = opFrac(pos + dur.quarterLength) - startNext: float|Fraction = end + startNext: OffsetQL = end isLast = (i == len(srcList) - 1) isFirst = (i == 0) diff --git a/music21/meter/core.py b/music21/meter/core.py index 80f9eec15..d71f6a650 100644 --- a/music21/meter/core.py +++ b/music21/meter/core.py @@ -368,7 +368,6 @@ def duration(self): >>> d.quarterLength 1.5 ''' - if self._overriddenDuration: return self._overriddenDuration else: @@ -414,9 +413,9 @@ def __init__( self._numerator: int = 1 # rationalized self._denominator: int = 0 # lowest common multiple - self._partition: list[MeterTerminal] = [] # a list of terminals or MeterSequences - self._overriddenDuration = None - self._levelListCache = {} + self._partition: list[MeterTerminal] = [] # a list of terminals (or MeterSequences?) + self._overriddenDuration: Duration|None = None + self._levelListCache: dict[tuple[int, bool], list[MeterTerminal]] = {} # this attribute is only used in MeterTerminals, and note # in MeterSequences; a MeterSequence's weight is based solely @@ -424,11 +423,11 @@ def __init__( # del self._weight -- no -- screws up pickling -- cannot del a slotted object # Bool stores whether this meter was provided as a summed numerator - self.summedNumerator = False + self.summedNumerator: bool = False # An optional parameter used only in meter display sequences. # Needed in cases where a meter component is parenthetical - self.parenthesis = False + self.parenthesis: bool = False if value is not None: self.load(value, partitionRequest) @@ -751,16 +750,14 @@ def partitionByList(self, numeratorList: Sequence[int] | Sequence[str]) -> None: Traceback (most recent call last): music21.exceptions21.MeterException: Cannot set partition by ['3/4', '1/8', '5/8'] ''' - optMatch = None + optMatch: MeterSequence | None | tuple[str, ...] = None # assume a list of terminal definitions if isinstance(numeratorList[0], str): # TODO: working with private methods of a created MeterSequence test = MeterSequence() - if t.TYPE_CHECKING: - numeratorList = cast(list[str], numeratorList) for mtStr in numeratorList: - test._addTerminal(mtStr) + test._addTerminal(t.cast(str, mtStr)) test._updateRatio() # if durations are equal, this can be used as a partition if self.duration.quarterLength == test.duration.quarterLength: @@ -771,9 +768,10 @@ def partitionByList(self, numeratorList: Sequence[int] | Sequence[str]) -> None: elif sum(t.cast(list[int], numeratorList)) in [self.numerator * x for x in range(1, 9)]: for i in range(1, 9): if sum(t.cast(list[int], numeratorList)) == self.numerator * i: - optMatch = [] + optMatchInner: list[str] = [] for n in numeratorList: - optMatch.append(f'{n}/{self.denominator * i}') + optMatchInner.append(f'{n}/{self.denominator * i}') + optMatch = tuple(optMatchINner) break # last resort: search options @@ -887,7 +885,7 @@ def partition( self.partitionByList(value) elif isinstance(value, MeterSequence): self.partitionByOtherMeterSequence(value) - elif common.isNum(value): + elif isinstance(value, int): self.partitionByCount(value, loadDefault=loadDefault) else: raise MeterException(f'cannot process partition argument {value}') @@ -1203,9 +1201,14 @@ def partitionStr(self): def load(self, value: str | MeterTerminal | Sequence[MeterTerminal | str], - partitionRequest: int | Sequence[str | MeterTerminal] | MeterSequence | None = None, + partitionRequest: (int + | Sequence[str] + | Sequence[MeterTerminal] + | Sequence[int] + | MeterSequence + | None) = None, autoWeight: bool = False, - targetWeight=None): + targetWeight: int|None = None): ''' This method is called when a MeterSequence is created, or if a MeterSequence is re-set. @@ -1384,14 +1387,6 @@ def weight(self, value: int | float | None) -> None: # environLocal.printDebug(['setting weight based on part, total, weight', # partRatio, totalRatio, mt.weight]) - @property - def numerator(self): - return self._numerator - - @property - def denominator(self): - return self._denominator - def _getFlatList(self): ''' Return a flattened version of this @@ -1401,7 +1396,6 @@ def _getFlatList(self): are generally immutable and thus it does not make sense to concatenate them. - >>> a = meter.MeterSequence('3/4') >>> a.partition(3) >>> b = a._getFlatList() @@ -1542,7 +1536,7 @@ def isUniformPartition(self, *, depth=0): # -------------------------------------------------------------------------- # alternative representations - def getLevelList(self, levelCount, flat=True): + def getLevelList(self, levelCount: int, flat: bool = True) -> list[MeterTerminal]: ''' Recursive utility function that gets everything at a certain level. @@ -1572,7 +1566,7 @@ def getLevelList(self, levelCount, flat=True): except KeyError: pass - mtList = [] + mtList: list[MeterTerminal] = [] for i in range(len(self._partition)): # environLocal.printDebug(['getLevelList weight', i, self[i].weight]) if not isinstance(self._partition[i], MeterSequence): diff --git a/music21/meter/tools.py b/music21/meter/tools.py index b77a72e0c..33abacee6 100644 --- a/music21/meter/tools.py +++ b/music21/meter/tools.py @@ -29,6 +29,7 @@ class MeterTerminalTuple(t.NamedTuple): denominator: int division: MeterDivision + NumDenom = tuple[int, int] NumDenomTuple = tuple[NumDenom, ...] MeterOptions = tuple[tuple[str, ...], ...] From 95095e208b0cba07b1e3b1401a024b5655277667 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 17 Sep 2024 16:14:50 -1000 Subject: [PATCH 7/8] fix lints --- music21/meter/base.py | 3 +- music21/meter/core.py | 216 ++++++++++++++++++++++++++++++++---------- 2 files changed, 166 insertions(+), 53 deletions(-) diff --git a/music21/meter/base.py b/music21/meter/base.py index 1f5eee29f..db971662f 100644 --- a/music21/meter/base.py +++ b/music21/meter/base.py @@ -1476,7 +1476,8 @@ def fixBeamsOneElementDepth(i: int, el: base.Music21Object, depth: int): beamType = 'partial-right' # if last in complete measure or not in a measure, always stop - elif isLast and (not srcStream.isMeasure or srcStream.paddingRight == 0.0): + elif (isLast and (not isinstance(srcStream, stream.Measure) + or srcStream.paddingRight == 0.0)): beamType = 'stop' # get a partial beam if we cannot form a beam if (beamPrevious is None diff --git a/music21/meter/core.py b/music21/meter/core.py index d71f6a650..f2567a342 100644 --- a/music21/meter/core.py +++ b/music21/meter/core.py @@ -46,7 +46,6 @@ class MeterTerminal(prebase.ProtoM21Object, SlottedObjectMixin): >>> a.duration.quarterLength 10.0 ''' - # CLASS VARIABLES # __slots__ = ( @@ -58,14 +57,14 @@ class MeterTerminal(prebase.ProtoM21Object, SlottedObjectMixin): ) # INITIALIZER # - def __init__(self, slashNotation: str|None = None, weight: int = 1): + def __init__(self, slashNotation: str|None = None, weight: int|float = 1): # because of how they are copied, MeterTerminals must not have any # initialization parameters without defaults self._duration: Duration|None = None self._numerator: int = 0 self._denominator: int = 1 - self._weight: int = 1 - self._overriddenDuration = None + self._weight: int|float = 1 # do not use setter here -- bad override in MeterSequence + self._overriddenDuration: Duration|None = None if slashNotation is not None: # assign directly to values, not properties, to avoid @@ -266,7 +265,7 @@ def subdivide( # properties @property - def weight(self): + def weight(self) -> float|int: ''' Return or set the weight of a MeterTerminal @@ -278,7 +277,7 @@ def weight(self): return self._weight @weight.setter - def weight(self, value): + def weight(self, value: float|int): self._weight = value @property @@ -406,15 +405,14 @@ class MeterSequence(MeterTerminal): def __init__( self, - value: str | MeterTerminal | Sequence[MeterTerminal | str] | None = None, + value: str | MeterTerminal | Sequence[MeterTerminal] | Sequence[str] | None = None, partitionRequest: t.Any|None = None, ): super().__init__() self._numerator: int = 1 # rationalized self._denominator: int = 0 # lowest common multiple - self._partition: list[MeterTerminal] = [] # a list of terminals (or MeterSequences?) - self._overriddenDuration: Duration|None = None + self._partition: list[MeterTerminal|MeterSequence] = [] self._levelListCache: dict[tuple[int, bool], list[MeterTerminal]] = {} # this attribute is only used in MeterTerminals, and note @@ -466,9 +464,9 @@ def __deepcopy__(self, memo=None): return new - def __getitem__(self, key): + def __getitem__(self, key: int) -> MeterTerminal: ''' - Get an MeterTerminal from _partition + Get an MeterTerminal (or MeterSequence) from _partition >>> a = meter.MeterSequence('4/4', 4) >>> a[3].numerator @@ -771,7 +769,7 @@ def partitionByList(self, numeratorList: Sequence[int] | Sequence[str]) -> None: optMatchInner: list[str] = [] for n in numeratorList: optMatchInner.append(f'{n}/{self.denominator * i}') - optMatch = tuple(optMatchINner) + optMatch = tuple(optMatchInner) break # last resort: search options @@ -1200,7 +1198,7 @@ def partitionStr(self): # loading is always destructive def load(self, - value: str | MeterTerminal | Sequence[MeterTerminal | str], + value: str | MeterTerminal | Sequence[MeterTerminal] | Sequence[str], partitionRequest: (int | Sequence[str] | Sequence[MeterTerminal] @@ -1208,7 +1206,7 @@ def load(self, | MeterSequence | None) = None, autoWeight: bool = False, - targetWeight: int|None = None): + targetWeight: int|float|None = None): ''' This method is called when a MeterSequence is created, or if a MeterSequence is re-set. @@ -1219,13 +1217,16 @@ def load(self, loading is a destructive operation. - >>> a = meter.MeterSequence() >>> a.load('4/4', 4) + >>> a + >>> str(a) '{1/4+1/4+1/4+1/4}' >>> a.load('4/4', 2) # request 2 beats + >>> a + >>> str(a) '{1/2+1/2}' @@ -1254,7 +1255,8 @@ def load(self, slashNotation = f'{n}/{d}' self._addTerminal(MeterTerminal(slashNotation)) self._updateRatio() - self.weight = targetWeight # may be None + if targetWeight is not None: + self.weight = targetWeight elif isinstance(value, MeterTerminal): # if we have a single MeterTerminal and autoWeight is active @@ -1273,7 +1275,8 @@ def load(self, # environLocal.printDebug('creating MeterSequence with %s' % obj) self._addTerminal(obj) self._updateRatio() - self.weight = targetWeight # may be None + if targetWeight is not None: + self.weight = targetWeight else: raise MeterException(f'cannot create a MeterSequence with a {value!r}') @@ -1362,30 +1365,27 @@ def weight(self) -> int | float: return summation @weight.setter - def weight(self, value: int | float | None) -> None: + def weight(self, value: int | float) -> None: # environLocal.printDebug(['calling setWeight with value', value]) + if not common.isNum(value): + raise MeterException('weight values must be numbers') - if value is None: - pass # do nothing - else: - if not common.isNum(value): - raise MeterException('weight values must be numbers') - try: - totalRatio = self._numerator / self._denominator - except TypeError: - raise MeterException( - 'Something wrong with the type of ' - + 'this numerator %s %s or this denominator %s %s' % - (self._numerator, type(self._numerator), - self._denominator, type(self._denominator))) - - for mt in self._partition: - # for mt in self: - partRatio = mt._numerator / mt._denominator - mt.weight = value * (partRatio / totalRatio) - # mt.weight = (partRatio/totalRatio) #* totalRatio - # environLocal.printDebug(['setting weight based on part, total, weight', - # partRatio, totalRatio, mt.weight]) + try: + totalRatio = self._numerator / self._denominator + except TypeError: + raise MeterException( + 'Something wrong with the type of ' + + 'this numerator %s %s or this denominator %s %s' % + (self._numerator, type(self._numerator), + self._denominator, type(self._denominator))) + + for mt in self._partition: + # for mt in self: + partRatio = mt._numerator / mt._denominator + mt.weight = value * (partRatio / totalRatio) + # mt.weight = (partRatio/totalRatio) #* totalRatio + # environLocal.printDebug(['setting weight based on part, total, weight', + # partRatio, totalRatio, mt.weight]) def _getFlatList(self): ''' @@ -1399,17 +1399,29 @@ def _getFlatList(self): >>> a = meter.MeterSequence('3/4') >>> a.partition(3) >>> b = a._getFlatList() + >>> b + [, + , + ] >>> len(b) 3 >>> a[1] = a[1].subdivide(4) - >>> a - >>> len(a) 3 + >>> a + + >>> b = a._getFlatList() >>> len(b) 6 + >>> b + [, + , + , + , + , + ] >>> a[1][2] = a[1][2].subdivide(4) >>> a @@ -1418,8 +1430,9 @@ def _getFlatList(self): >>> len(b) 9 ''' + # Is this the same as getLevelList(0)? mtList = [] - for obj in self._partition: + for obj in self._partition: # or for obj in self if not isinstance(obj, MeterSequence): mtList.append(obj) else: # its a meter sequence @@ -1437,15 +1450,29 @@ def flatten(self) -> MeterSequence: ''' Return a new MeterSequence composed of the flattened representation. + Here a sequence is already flattened: + >>> ms = meter.MeterSequence('3/4', 3) + >>> ms + >>> b = ms.flatten() + >>> b + >>> len(b) 3 + >>> b is ms + False + + Now take the original MeterSequence and subdivide the second beat into 4 parts: >>> ms[1] = ms[1].subdivide(4) + >>> ms + >>> b = ms.flatten() >>> len(b) 6 + >>> b + >>> ms[1][2] = ms[1][2].subdivide(4) >>> ms @@ -1453,6 +1480,8 @@ def flatten(self) -> MeterSequence: >>> b = ms.flatten() >>> len(b) 9 + >>> b + ''' post = MeterSequence() post.load(self._getFlatList()) @@ -1540,17 +1569,65 @@ def getLevelList(self, levelCount: int, flat: bool = True) -> list[MeterTerminal ''' Recursive utility function that gets everything at a certain level. + If flat is True, it guarantees to return a list of MeterTerminals and not + MeterSequences. Otherwise, there may be Sequences in there. + + Example: a Sequence representing something in 4/4 divided as + 1 quarter, 2 eighth, 1 quarter, ((2-sixteenths) + 1 eighth). + >>> b = meter.MeterSequence('4/4', 4) >>> b[1] = b[1].subdivide(2) >>> b[3] = b[3].subdivide(2) >>> b[3][0] = b[3][0].subdivide(2) >>> b + + Get the top level of this structure, flattening everything underneath: + >>> b.getLevelList(0) [, , , ] + + One level down: + + >>> b.getLevelList(1) + [, + , + , + , + , + ] + + Without flattening, first two levels: + + >>> b.getLevelList(0, flat=False) + [, + , + , + ] + + (Note that levelList 0, flat=False is essentially the same as iterating + over a MeterSequence) + + >>> list(b) + [, + , + , + ] + + + >>> b.getLevelList(1, flat=False) + [, + , + , + , + , + ] + + Generally, these level lists will be converted back to MeterSequences: + >>> meter.MeterSequence(b.getLevelList(0)) >>> meter.MeterSequence(b.getLevelList(1)) @@ -1559,36 +1636,71 @@ def getLevelList(self, levelCount: int, flat: bool = True) -> list[MeterTerminal >>> meter.MeterSequence(b.getLevelList(3)) + + OMIT_FROM_DOCS + + Test that cache is used and does not get manipulated + + >>> b = meter.MeterSequence('3/4', 3) + >>> (0, True) in b._levelListCache + False + >>> o = b.getLevelList(0) + + Mess with the list: + + >>> o.append(meter.core.MeterTerminal('1/8')) + >>> o + [, + , + , + ] + + Cache is populated: + + >>> (0, True) in b._levelListCache + True + + But a new list is created. + + >>> b.getLevelList(0) + [, + , + ] + + >>> b.getLevelList(0)[0] is o[0] + True ''' cacheKey = (levelCount, flat) try: # check in cache - return self._levelListCache[cacheKey] + return list(tuple(self._levelListCache[cacheKey])) except KeyError: pass mtList: list[MeterTerminal] = [] for i in range(len(self._partition)): # environLocal.printDebug(['getLevelList weight', i, self[i].weight]) - if not isinstance(self._partition[i], MeterSequence): - mt = self[i] # a meter terminal + partition_i: MeterTerminal|MeterSequence = self._partition[i] + if not isinstance(partition_i, MeterSequence): + mt = self[i] # a MeterTerminal mtList.append(mt) - else: # it is a sequence + else: # it is a MeterSequence if levelCount > 0: # retain this sequence but get lower level # reduce level by 1 when recursing; do not # change levelCount here - mtList += self._partition[i].getLevelList( + mtList += partition_i.getLevelList( levelCount - 1, flat) else: # level count is at zero if flat: # make sequence into a terminal mt = MeterTerminal('%s/%s' % ( - self._partition[i].numerator, self._partition[i].denominator)) + partition_i.numerator, partition_i.denominator)) # set weight to that of the sequence - mt.weight = self._partition[i].weight + mt.weight = partition_i.weight mtList.append(mt) else: # it is not a terminal, it is a meter sequence - mtList.append(self._partition[i]) - # store in cache - self._levelListCache[cacheKey] = mtList + mtList.append(partition_i) + + # store in cache but let this be manipulated + self._levelListCache[cacheKey] = list(tuple(mtList)) return mtList def getLevel(self, level=0, flat=True): From f3fac08d4aa0e3d7b7dddb9a1b756fbdcab93750 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 17 Sep 2024 17:25:18 -1000 Subject: [PATCH 8/8] mypy --- music21/meter/base.py | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/music21/meter/base.py b/music21/meter/base.py index db971662f..ce280bd5a 100644 --- a/music21/meter/base.py +++ b/music21/meter/base.py @@ -985,7 +985,8 @@ def beatDivisionCount(self) -> int: return 1 # need to see if first-level subdivisions are partitioned - if not isinstance(self.beatSequence[0], MeterSequence): + beat_seq_0 = self.beatSequence[0] + if not isinstance(beat_seq_0, MeterSequence): return 1 # getting length here gives number of subdivisions @@ -994,7 +995,7 @@ def beatDivisionCount(self) -> int: # convert this to a set; if length is 1, then all beats are uniform if len(set(post)) == 1: - return len(self.beatSequence[0]) # all are the same + return len(beat_seq_0) # all are the same else: return 1 @@ -1054,18 +1055,40 @@ def beatDivisionDurations(self) -> list[duration.Duration]: Value returned of non-uniform beat divisions will change at any time after v7.1 to avoid raising an exception. + + OMIT_FROM_DOCS + + Previously a time signature with beatSequence containing only + MeterTerminals would raise exceptions. + + >>> ts = meter.TimeSignature('2/128') + >>> ts.beatSequence[0] + + >>> ts.beatDivisionDurations + [] + + >>> ts = meter.TimeSignature('1/128') + >>> ts.beatSequence[0] + + >>> ts.beatDivisionDurations + [] ''' post = [] - if len(self.beatSequence) == 1: - raise TimeSignatureException( - 'cannot determine beat division for a non-partitioned beat') for mt in self.beatSequence: - for subMt in mt: - post.append(subMt.duration.quarterLength) + if isinstance(mt, MeterSequence): + for subMt in mt: + post.append(subMt.duration.quarterLength) + else: + post.append(mt.duration.quarterLength) if len(set(post)) == 1: # all the same out = [] - for subMt in self.beatSequence[0]: - out.append(subMt.duration) + beat_seq_0 = self.beatSequence[0] + if isinstance(beat_seq_0, MeterSequence): + for subMt in beat_seq_0: + if subMt.duration is not None: # should not be: + out.append(subMt.duration) + elif beat_seq_0.duration is not None: # MeterTerminal w/ non-empty duration. + out.append(beat_seq_0.duration) return out else: raise TimeSignatureException(f'non uniform beat division: {post}')