diff --git a/music21/_version.py b/music21/_version.py index 240ab9a1f..d411b2944 100644 --- a/music21/_version.py +++ b/music21/_version.py @@ -47,7 +47,7 @@ ''' from __future__ import annotations -__version__ = '11.0.0b8' +__version__ = '11.0.0b9' def get_version_tuple(vv): v = vv.split('.') diff --git a/music21/base.py b/music21/base.py index 17d9f0a97..c00c214eb 100644 --- a/music21/base.py +++ b/music21/base.py @@ -26,7 +26,7 @@ >>> music21.VERSION_STR -'11.0.0b8' +'11.0.0b9' Alternatively, after doing a complete import, these classes are available under the module "base": diff --git a/music21/braille/basic.py b/music21/braille/basic.py index 7eb02e4ef..22d600e97 100644 --- a/music21/braille/basic.py +++ b/music21/braille/basic.py @@ -453,7 +453,7 @@ def keySigToBraille(music21KeySignature, outgoingKeySig=None): f'Incoming Key Signature {music21KeySignature} cannot be transcribed to braille.' ) music21KeySignature.editorial.brailleEnglish.append( - f'Key Signature {music21KeySignature} sharps None' + f'Key Signature {music21KeySignature} cannot be transcribed' ) return symbols['basic_exception'] diff --git a/music21/duration.py b/music21/duration.py index 811087db9..38afca337 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -3452,6 +3452,7 @@ def fixBrokenTupletDuration(self, tupletGroup: list[note.GeneralNote]) -> None: There is a side format of humdrum that the Josquin Research Project uses for long notes like the 3%2. + >>> saved_JRP_flavor = humdrum.spineParser.flavors['JRP'] #_DOCS_HIDE >>> humdrum.spineParser.flavors['JRP'] = True Since Humdrum parsing is going to apply TupletFixer, we will temporarily @@ -3475,6 +3476,7 @@ def fixBrokenTupletDuration(self, tupletGroup: list[note.GeneralNote]) -> None: {10.6667 - 12.0} >>> duration.TupletFixer.fixBrokenTupletDuration = saved_fixed_broken + >>> humdrum.spineParser.flavors['JRP'] = saved_JRP_flavor #_DOCS_HIDE >>> tf = duration.TupletFixer(m1) >>> tupletGroups = tf.findTupletGroups(incorporateGroupings=True) >>> tupletGroups diff --git a/music21/key.py b/music21/key.py index 6085d9a4d..b23054f51 100644 --- a/music21/key.py +++ b/music21/key.py @@ -85,7 +85,7 @@ def convertKeyStringToMusic21KeyString(textString): return textString -def sharpsToPitch(sharpCount): +def sharpsToPitch(sharpCount: int) -> pitch.Pitch: ''' Given a positive/negative number of sharps, return a Pitch object set to the appropriate major key value. @@ -122,9 +122,6 @@ def sharpsToPitch(sharpCount): >>> key._sharpsToPitchCache[1] ''' - if sharpCount is None: - sharpCount = 0 # fix for C major - if sharpCount in _sharpsToPitchCache: # return a deepcopy of the pitch return copy.deepcopy(_sharpsToPitchCache[sharpCount]) @@ -302,28 +299,16 @@ class KeySignature(base.Music21Object): >>> legal - To set a non-traditional Key Signature, create a KeySignature object - with `sharps=None`, and then set the `alteredPitches` list: - - >>> unusual = key.KeySignature(sharps=None) - >>> unusual.alteredPitches = ['E-', 'G#'] - >>> unusual - - >>> unusual.isNonTraditional - True - - To set a pitch as displayed in a particular octave, create a non-traditional - KeySignature and then set pitches with octaves: + To set a non-traditional Key Signature, set `isNonTraditional` to True and + give the `alteredPitches` list. A pitch with an octave is displayed in that + octave; set `accidentalsApplyOnlyToOctave` to True if it should affect only + that octave: - >>> unusual = key.KeySignature(sharps=None) - >>> unusual.alteredPitches = ['F#4'] + >>> unusual = key.KeySignature() + >>> unusual.isNonTraditional = True + >>> unusual.alteredPitches = ['E-', 'G#4'] >>> unusual - - - If the accidental applies to all octaves but is being displayed differently - then you are done, but if you want them to apply only to the octave displayed - in then set `.accidentalsApplyOnlyToOctave` to `True`: - + >>> unusual.accidentalsApplyOnlyToOctave False >>> unusual.accidentalsApplyOnlyToOctave = True @@ -343,9 +328,11 @@ class KeySignature(base.Music21Object): * Changed in v7: `sharps` defaults to 0 (key of no flats/sharps) rather than `None` for nontraditional keys. + * Changed in v11: `sharps` is always an int; `isNonTraditional` is a + settable attribute. Passing `sharps=None` is deprecated. ''' _styleClass = style.TextStyle - equalityAttributes = ('sharps',) + equalityAttributes = ('sharps', 'isNonTraditional') # note that musicxml permits non-traditional keys by specifying # one or more altered tones; these are given as pairs of @@ -353,12 +340,20 @@ class KeySignature(base.Music21Object): classSortOrder = 2 - def __init__(self, sharps: int|None = 0, **keywords): + def __init__(self, sharps: int = 0, **keywords): super().__init__(**keywords) - # position on the circle of fifths, where 1 is one sharp, -1 is one flat + self.isNonTraditional: bool = False + + if sharps is None: + warnings.warn( + 'sharps=None is deprecated: set isNonTraditional to True instead.', + exceptions21.Music21DeprecationWarning, + stacklevel=2) + sharps = 0 + self.isNonTraditional = True try: - if sharps is not None and (sharps != int(sharps)): + if sharps != int(sharps): raise KeySignatureException( f'Cannot get a KeySignature from this "number" of sharps: {sharps!r}; ' + 'did you mean to use a key.Key() object instead?') @@ -368,6 +363,7 @@ def __init__(self, sharps: int|None = 0, **keywords): + 'did you mean to use a key.Key() object instead?' ) from ve + # position on the circle of fifths, where 1 is one sharp, -1 is one flat self._sharps = sharps # need to store a list of pitch objects, used for creating a # non-traditional key @@ -379,7 +375,7 @@ def __init__(self, sharps: int|None = 0, **keywords): def _strDescription(self): output = '' ns = self.sharps - if ns is None: + if self.isNonTraditional: output = 'pitches: [' + ', '.join([str(p) for p in self.alteredPitches]) + ']' elif ns > 1: output = f'{ns} sharps' @@ -396,7 +392,7 @@ def _strDescription(self): def _reprInternal(self): return 'of ' + self._strDescription() - def asKey(self, mode: str|None = None, tonic: str|None = None): + def asKey(self, mode: str|None = None, tonic: str|None = None) -> Key: ''' Return a `key.Key` object representing this KeySignature object as a key in the given mode or in the given tonic. If `mode` is None, and `tonic` is not provided, @@ -429,7 +425,6 @@ def asKey(self, mode: str|None = None, tonic: str|None = None): * New in v7: `tonic` argument to solve for mode. ''' - our_sharps = self.sharps or 0 # || 0 in case of None -- non-standard key-signature if mode is not None and tonic is not None: warnings.warn(f'ignoring provided tonic: {tonic}', KeyWarning, stacklevel=2) if mode is None and tonic is None: @@ -438,7 +433,7 @@ def asKey(self, mode: str|None = None, tonic: str|None = None): majorSharpsToMode = {v: k for k, v in modeSharpsAlter.items()} majorSharps = pitchToSharps(tonic) try: - mode = majorSharpsToMode[our_sharps - majorSharps] + mode = majorSharpsToMode[self.sharps - majorSharps] except KeyError as ke: raise KeyException( f'Could not solve for mode from sharps={self.sharps}, tonic={tonic}') from ke @@ -448,7 +443,7 @@ def asKey(self, mode: str|None = None, tonic: str|None = None): raise KeyException(f'Mode {mode} is unknown') sharpAlterationFromMajor = modeSharpsAlter[mode] - pitchObj = sharpsToPitch(our_sharps - sharpAlterationFromMajor) + pitchObj = sharpsToPitch(self.sharps - sharpAlterationFromMajor) return Key(pitchObj.name, mode) @@ -491,7 +486,8 @@ def alteredPitches(self) -> list[pitch.Pitch]: Non-standard, non-traditional key signatures can set their own altered pitches cache. - >>> nonTrad = key.KeySignature(sharps=None) + >>> nonTrad = key.KeySignature() + >>> nonTrad.isNonTraditional = True >>> nonTrad.alteredPitches = ['B-', 'F#', 'E-', 'G#'] >>> nonTrad.alteredPitches [, @@ -503,7 +499,8 @@ def alteredPitches(self) -> list[pitch.Pitch]: Ensure at least something is provided when the user hasn't provided enough info: - >>> nonTrad2 = key.KeySignature(sharps=None) + >>> nonTrad2 = key.KeySignature() + >>> nonTrad2.isNonTraditional = True >>> nonTrad2.alteredPitches [] @@ -512,7 +509,7 @@ def alteredPitches(self) -> list[pitch.Pitch]: return self._alteredPitches post: list[pitch.Pitch] = [] - if self.sharps is None: + if self.isNonTraditional: return post if self.sharps > 0: @@ -536,7 +533,7 @@ def alteredPitches(self) -> list[pitch.Pitch]: return post @alteredPitches.setter - def alteredPitches(self, newAlteredPitches: list[str|pitch.Pitch|note.Note] + def alteredPitches(self, newAlteredPitches: t.Iterable[str|pitch.Pitch|note.Note] ) -> None: self.clearCache() newList: list[pitch.Pitch] = [] @@ -549,31 +546,6 @@ def alteredPitches(self, newAlteredPitches: list[str|pitch.Pitch|note.Note] newList.append(copy.deepcopy(p.pitch)) self._alteredPitches = newList - @property - def isNonTraditional(self) -> bool: - ''' - Returns bool if this is a non-traditional KeySignature: - - >>> g = key.KeySignature(3) - >>> g.isNonTraditional - False - - >>> g = key.KeySignature(sharps=None) - >>> g.alteredPitches = [pitch.Pitch('E`')] - >>> g.isNonTraditional - True - - >>> g - - - >>> g.accidentalByStep('E') - - ''' - if self.sharps is None and self.alteredPitches: - return True - else: - return False - def accidentalByStep(self, step: StepName) -> pitch.Accidental|None: ''' Given a step (C, D, E, F, etc.) return the accidental @@ -844,16 +816,9 @@ def getScale(self, mode='major'): # -------------------------------------------------------------------------- # properties - def _getSharps(self) -> int|None: - return self._sharps - - def _setSharps(self, value: int|None): - if value != self._sharps: - self._sharps = value - self.clearCache() - - sharps = property(_getSharps, _setSharps, - doc=''' + @property + def sharps(self) -> int: + ''' Get or set the number of sharps. If the number is negative then it sets the number of flats. Equivalent to musicxml's 'fifths' attribute. @@ -864,9 +829,14 @@ def _setSharps(self, value: int|None): >>> ks1.sharps = -4 >>> ks1 + ''' + return self._sharps - Can be set to None for a non-traditional key signature - ''') + @sharps.setter + def sharps(self, value: int) -> None: + if value != self._sharps: + self._sharps = value + self.clearCache() class Key(KeySignature, scale.DiatonicScale): @@ -1416,6 +1386,34 @@ def testAsKey(self): # test exception chained from KeyError self.assertIsInstance(cm.exception.__cause__, KeyError) + def testNonTraditional(self): + ''' + AI-assisted (Claude). + ''' + from music21 import key + + ks = key.KeySignature(3) + self.assertFalse(ks.isNonTraditional) + + ks = key.KeySignature() + ks.isNonTraditional = True + ks.alteredPitches = [pitch.Pitch('E`')] + self.assertEqual(repr(ks), '') + self.assertEqual(ks.accidentalByStep('E'), pitch.Accidental('half-flat')) + + # a non-traditional key signature is not equal to the C-major signature + # it shares a `sharps` count with. + self.assertNotEqual(ks, key.KeySignature()) + + def testSharpsNoneDeprecated(self): + ''' + AI-assisted (Claude). + ''' + with self.assertWarns(exceptions21.Music21DeprecationWarning): + ks = KeySignature(sharps=None) + self.assertEqual(ks.sharps, 0) + self.assertTrue(ks.isNonTraditional) + # ------------------------------------------------------------------------------ # define presented order in documentation diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 034c21557..d859d1ff6 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -7340,7 +7340,7 @@ def timeSignatureToXml(self, ts: meter.TimeSignature|meter.SenzaMisuraTimeSignat self.setPrintObject(mxTime, ts) return mxTime - def keySignatureToXml(self, keyOrKeySignature): + def keySignatureToXml(self, keyOrKeySignature: key.KeySignature) -> Element: # noinspection PyShadowingNames ''' returns a key tag from a music21 @@ -7365,7 +7365,8 @@ def keySignatureToXml(self, keyOrKeySignature): major - >>> ksNonTrad = key.KeySignature(sharps=None) + >>> ksNonTrad = key.KeySignature() + >>> ksNonTrad.isNonTraditional = True >>> ksNonTrad.alteredPitches = ['C#', 'E-4'] >>> ksNonTrad diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index fdc7b0a22..8c0970be6 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -6250,7 +6250,7 @@ def mxKeyOctaves(self, mxKey, ks): ks.alteredPitches = alteredPitches - def nonTraditionalKeySignature(self, mxKey): + def nonTraditionalKeySignature(self, mxKey: ET.Element) -> key.KeySignature: # noinspection PyShadowingNames ''' Returns a KeySignature object that represents a nonTraditional Key Signature @@ -6279,17 +6279,17 @@ def nonTraditionalKeySignature(self, mxKey): children = list(mxKey) lastTag = None - steps = [] - alters = [] - accidentals = [] + steps: list[str] = [] + alters: list[float] = [] + accidentals: list[str|None] = [] for c in children: tag = c.tag if lastTag == 'key-alter' and tag == 'key-step': accidentals.append(None) - if tag == 'key-step': + if tag == 'key-step' and c.text: steps.append(c.text) - elif tag == 'key-alter': + elif tag == 'key-alter' and c.text: alters.append(float(c.text)) elif tag == 'key-accidental': accidentals.append(c.text) @@ -6301,9 +6301,10 @@ def nonTraditionalKeySignature(self, mxKey): raise MusicXMLImportException( 'For non traditional signatures each step must have an alter') - ks = key.KeySignature(sharps=None) + ks = key.KeySignature() + ks.isNonTraditional = True - alteredPitches = [] + alteredPitches: list[pitch.Pitch] = [] for step, alter, accidental in zip(steps, alters, accidentals): p = pitch.Pitch(step) if accidental is not None: