Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion music21/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
'''
from __future__ import annotations

__version__ = '11.0.0b8'
__version__ = '11.0.0b9'

def get_version_tuple(vv):
v = vv.split('.')
Expand Down
2 changes: 1 addition & 1 deletion music21/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<class 'music21.base.Music21Object'>

>>> music21.VERSION_STR
'11.0.0b8'
'11.0.0b9'

Alternatively, after doing a complete import, these classes are available
under the module "base":
Expand Down
2 changes: 1 addition & 1 deletion music21/braille/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down
2 changes: 2 additions & 0 deletions music21/duration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -3475,6 +3476,7 @@ def fixBrokenTupletDuration(self, tupletGroup: list[note.GeneralNote]) -> None:
{10.6667 - 12.0} <music21.note.Note F#>

>>> 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
Expand Down
146 changes: 72 additions & 74 deletions music21/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -122,9 +122,6 @@ def sharpsToPitch(sharpCount):
>>> key._sharpsToPitchCache[1]
<music21.pitch.Pitch G>
'''
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])
Expand Down Expand Up @@ -302,28 +299,16 @@ class KeySignature(base.Music21Object):
>>> legal
<music21.key.Key of c# minor>

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
<music21.key.KeySignature of pitches: [E-, G#]>
>>> 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
<music21.key.KeySignature of pitches: [F#4]>

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`:

<music21.key.KeySignature of pitches: [E-, G#4]>
>>> unusual.accidentalsApplyOnlyToOctave
False
>>> unusual.accidentalsApplyOnlyToOctave = True
Expand All @@ -343,22 +328,32 @@ 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
# step names and semitone alterations

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?')
Expand All @@ -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
Expand All @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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
[<music21.pitch.Pitch B->,
Expand All @@ -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
[]

Expand All @@ -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:
Expand All @@ -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] = []
Expand All @@ -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
<music21.key.KeySignature of pitches: [E`]>

>>> g.accidentalByStep('E')
<music21.pitch.Accidental half-flat>
'''
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
Expand Down Expand Up @@ -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.
Expand All @@ -864,9 +829,14 @@ def _setSharps(self, value: int|None):
>>> ks1.sharps = -4
>>> ks1
<music21.key.KeySignature of 4 flats>
'''
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):
Expand Down Expand Up @@ -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), '<music21.key.KeySignature of pitches: [E`]>')
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
Expand Down
5 changes: 3 additions & 2 deletions music21/musicxml/m21ToXml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -7365,7 +7365,8 @@ def keySignatureToXml(self, keyOrKeySignature):
<mode>major</mode>
</key>

>>> ksNonTrad = key.KeySignature(sharps=None)
>>> ksNonTrad = key.KeySignature()
>>> ksNonTrad.isNonTraditional = True
>>> ksNonTrad.alteredPitches = ['C#', 'E-4']
>>> ksNonTrad
<music21.key.KeySignature of pitches: [C#, E-4]>
Expand Down
17 changes: 9 additions & 8 deletions music21/musicxml/xmlToM21.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down
Loading