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
3 changes: 3 additions & 0 deletions music21/musicxml/xmlToM21.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ def setStyleAttributes(self, mxObject, m21Object, musicXMLNames, m21Names=None):
>>> m21Obj.style.hideObjectOnPrint
True
'''
if not mxObject.attrib:
return

if isinstance(m21Object, style.Style):
stObj = m21Object
else:
Expand Down
60 changes: 50 additions & 10 deletions music21/pitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1639,6 +1639,12 @@ def fullName(self) -> str:


# ------------------------------------------------------------------------------
# strings that Pitch.name parsed: (step, accidental string or None, octave or None).
# Holds no Accidental objects, since those are mutable. Cleared when full.
_pitchNameCache: dict[str, tuple[StepName, str|None, int|None]] = {}
_pitchNameCacheSize = 1000


# tried as SlottedObjectMixin -- made creation time slower! Not worth the restrictions
class Pitch(prebase.ProtoM21Object):
'''
Expand Down Expand Up @@ -2811,6 +2817,8 @@ def name(self) -> str:
>>> a = pitch.Pitch('B---')
>>> a.name
'B---'

* Changed in v11: a name longer than 100 characters raises a ValueError.
'''
if self.accidental is not None:
return self.step + self.accidental.modifier
Expand All @@ -2823,10 +2831,38 @@ def name(self, usrStr: str) -> None:
Set name, which may be provided with or without octave values. C4 or D-3
are both accepted.
'''
try:
step, accidentalStr, octave = _pitchNameCache[usrStr]
except (KeyError, TypeError): # TypeError: not hashable, so not a str
self._cacheParsedName(usrStr)
step, accidentalStr, octave = _pitchNameCache[usrStr]
self._step = step
self.spellingIsInferred = False
self._accidental = (None if accidentalStr is None
else Accidental(accidentalStr))
if octave is not None:
self._octave = octave
self.informClient()

def _cacheParsedName(self, usrStr: str) -> None:
'''
Parse a name not yet seen and remember its step, accidental, and
octave for the `.name` setter. Raises if it is not a name.

>>> p = pitch.Pitch()
>>> p._cacheParsedName(' f#5')
>>> pitch._pitchNameCache[' f#5']
('F', '#', 5)
'''
cacheKey = usrStr
try:
usrStr = usrStr.strip()
except AttributeError:
raise ValueError(f'Argument to name, {usrStr!r}, must be a string, not {type(usrStr)}.')
# remembered names are kept, so do not keep huge ones
if len(cacheKey) > 100:
raise ValueError(
f'Argument to name must be at most 100 characters, not {len(cacheKey)}.')

# extract any numbers that may be octave designations
octFound: list[str] = []
Expand All @@ -2843,20 +2879,24 @@ def name(self, usrStr: str) -> None:
octNot.append(char)
usrStr = ''.join(octNot)
octFoundStr = ''.join(octFound)
# we have nothing but pitch specification
if len(usrStr) == 1:
self.step = usrStr # type: ignore
self.accidental = None
# assume everything following pitch is accidental specification
elif len(usrStr) > 1:
self.step = usrStr[0] # type: ignore
self.accidental = Accidental(usrStr[1:])
else:
if not usrStr:
raise PitchException(f'Cannot make a name out of {usrStr!r}')
step = t.cast(StepName, usrStr[0].upper())
if step not in STEPNAMES:
raise PitchException(f'Cannot make a step out of {step!r}')
# assume everything following pitch is accidental specification
accidentalStr: str|None = None
if len(usrStr) > 1:
accidentalStr = usrStr[1:]
Accidental(accidentalStr) # raises if it is not an accidental

octave: int|None = None
if octFoundStr: # bool('0') == True, so okay
octave = int(octFoundStr)
self.octave = octave

if len(_pitchNameCache) >= _pitchNameCacheSize:
_pitchNameCache.clear()
_pitchNameCache[cacheKey] = (step, accidentalStr, octave)

@property
def unicodeName(self) -> str:
Expand Down
42 changes: 42 additions & 0 deletions music21/test/test_pitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,48 @@ def testNameSetting(self):
):
p.name = 32

def testNameCache(self):
'''
A name is parsed once and remembered; names that cannot be parsed are not.
'''
with mock.patch.dict(pitch._pitchNameCache, clear=True):
first = Pitch('E-6')
self.assertEqual(pitch._pitchNameCache, {'E-6': ('E', '-', 6)})
second = Pitch('E-6')
self.assertEqual(second.nameWithOctave, 'E-6')
# Accidentals can be changed, so each Pitch gets its own
self.assertIsNot(second.accidental, first.accidental)

# a name that cannot be parsed changes nothing
with self.assertRaises(AccidentalException):
second.name = 'D$'
self.assertEqual(second.nameWithOctave, 'E-6')
with self.assertRaisesRegex(ValueError, 'must be a string'):
second.name = ['C4'] # not hashable
with self.assertRaisesRegex(ValueError, 'at most 100 characters'):
second.name = 'C4'.center(101) # padding counts
self.assertEqual(list(pitch._pitchNameCache), ['E-6'])
self.assertEqual(Pitch('C4'.center(100)).nameWithOctave, 'C4')

# cleared when full
with mock.patch.object(pitch, '_pitchNameCacheSize', 1):
Pitch('F4')
self.assertEqual(list(pitch._pitchNameCache), ['F4'])

def testNameSetterInformsNoteOnce(self):
'''
Setting a name without an octave, even one not seen before, keeps the
octave and the microtone, makes the spelling explicit, and tells the Note once.
'''
n = note.Note(73) # C#5, spelling inferred
n.pitch.microtone = 20
with (mock.patch.dict(pitch._pitchNameCache, clear=True),
mock.patch.object(n, 'pitchChanged') as pitchChanged):
n.pitch.name = 'B-'
pitchChanged.assert_called_once()
self.assertEqual(str(n.pitch), 'B-5(+20c)')
self.assertFalse(n.pitch.spellingIsInferred)

def testInitShortcutsMatchParsing(self):
# 'C', 'C4', and step= skip the name and step setters
self.assertEqual(Pitch('C'), Pitch('c'))
Expand Down
Loading