From 0cf34a98663cc09cfdef4e8d283aa527f56820de Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 6 Aug 2026 15:44:00 -1000 Subject: [PATCH 1/4] lily: add typing throughout; fix invalid LilyPond output Annotate every __init__, method and non-obvious local in lilyObjects.py and translate.py. stringOutput() is now `-> str` on all ~90 grammar classes. Eight subclasses returned None for empty contents, which is unrepresentable in an override and was also a latent crash: LyObject.__str__ does stringOutput().replace(...), so str(LyBookBody()) raised AttributeError. They return '' now. In translate.py the .classes string dispatch in loadFromMusic21Object and appendM21ObjectToContext became isinstance checks -- that is what lets mypy verify the dispatch -- and self.context.contents access moved behind contextContents()/setContextContents(), since context is legitimately polymorphic (LyLilypondTop, LyMusicList, LyCompositeMusic). LilyPond correctness, each checked against an installed LilyPond 2.24.4 rather than from memory. All four produced silently wrong or unparseable output: - barlineDict: 'dotted' was ":", 'dashed' was "dashed", 'heavy-heavy' was ".|.", and the repeats were "|:"/":|". None of those are defined in 2.24's bar-line.scm, and LilyPond draws an unknown bar name as nothing at all with no warning. Now ";", "!", "..", ".|:", ":|.". 'short' was commented out entirely, so a `short` barline raised KeyError; mapped to ",". All 11 entries of bar.barTypeList now round-trip. - \markuplines was renamed \markuplist in 2.16; the old name is a hard parse error today. - LyTempoRange joined a range with "~", a tie: `\tempo 4 = 70~100` fails with "not a duration" plus an unterminated-tie warning. Uses "-". - LyModeChangingHead emitted \notes for mode='note', which has never existed. Always \notemode for that mode; the other four shorthands are valid. Not changed, having verified they still work in 2.24: the pre-2.18 (parser location) music-function signatures in fictaDef/colorDef, and \times (deprecated for \tuplet but still valid). \[ \] and \~ are faithful to the grammar this file follows -- ligature brackets and the pes-or-flexa event -- so those only gained comments. Three bugs surfaced by the annotations: - LyOutputDefBody.stringOutput had an inverted guard: it raised when outputDefBody was set and dereferenced None when it was not. - LyMarkupBracedListBody.stringOutput called super().__init__(), resetting the object mid-render. - LilypondConverter.__init__ set self.LILYEXEC = None after setupTools() had already populated it. No version bump: lily is output-only, so no parse caches are invalidated. AI-assisted (Claude) --- music21/lily/lilyObjects.py | 748 ++++++++++++++++++++++-------------- music21/lily/translate.py | 471 ++++++++++++----------- 2 files changed, 718 insertions(+), 501 deletions(-) diff --git a/music21/lily/lilyObjects.py b/music21/lily/lilyObjects.py index c9a4eeb24..d0ba53ac2 100644 --- a/music21/lily/lilyObjects.py +++ b/music21/lily/lilyObjects.py @@ -20,11 +20,15 @@ import typing as t import unittest +import weakref from music21 import common from music21 import exceptions21 from music21 import prebase +if t.TYPE_CHECKING: + from music21 import base + class LilyObjectsException(exceptions21.Music21Exception): pass @@ -38,24 +42,26 @@ class LyObject(prebase.ProtoM21Object): >>> lyo.stringOutput() '' + * Changed in v11: `stringOutput()` always returns a `str`; the subclasses that + returned None for empty contents now return `''`. ''' - supportedClasses: list[object] = [] # ordered list of classes to support - m21toLy: dict[str, dict] = {} + supportedClasses: list[str] = [] # ordered list of classes to support + m21toLy: dict[str, dict[str, str]] = {} defaultAttributes: dict[str, t.Any] = {} backslash = '\\' - def __init__(self): + def __init__(self) -> None: # self.context = context - self.lilyAttributes = {} - self._parent = None + self.lilyAttributes: dict[str, t.Any] = {} + self._parent: weakref.ReferenceType[LyObject]|None = None self.thisIndent = 0 - self.markupTop = None - self.lyricMarkupOrIdentifier = None - self.markupListOrIdentifier = None - self.markupTopOrIdentifier = None + self.markupTop: LyObject|str|None = None + self.lyricMarkupOrIdentifier: LyObject|str|None = None + self.markupListOrIdentifier: LyObject|str|None = None + self.markupTopOrIdentifier: LyObject|str|None = None # self.setLilyAttributes(inObject, context, **keywords) - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: t.Any) -> None: if isinstance(value, LyObject): value.setParent(self) elif common.isIterable(value): @@ -66,15 +72,15 @@ def __setattr__(self, name, value): object.__setattr__(self, name, value) - def getParent(self): - if self._parent is not None: - actualParent = common.unwrapWeakref(self._parent) - return actualParent + def getParent(self) -> LyObject|None: + if self._parent is None: + return None + return self._parent() - def setParent(self, parentObject): - self._parent = common.wrapWeakref(parentObject) + def setParent(self, parentObject: LyObject) -> None: + self._parent = weakref.ref(parentObject) - def ancestorList(self): + def ancestorList(self) -> list[LyObject]: r''' returns a list of all unwrapped parent objects for the current object ''' @@ -85,7 +91,11 @@ def ancestorList(self): currentParent = currentParent.getParent() return ancestors - def getAncestorByClass(self, classObj, getAncestorNumber=1): + def getAncestorByClass( + self, + classObj: type[LyObject], + getAncestorNumber: int = 1, + ) -> LyObject|None: currentIter = 1 for a in self.ancestorList(): if isinstance(a, classObj): @@ -96,7 +106,7 @@ def getAncestorByClass(self, classObj, getAncestorNumber=1): return None @property - def newlineIndent(self): + def newlineIndent(self) -> str: # totalIndents = self.thisIndent ancestors = self.ancestorList() # for ancestor in ancestors: @@ -105,7 +115,7 @@ def newlineIndent(self): indentSpaces = ' ' * totalIndents return '\n' + indentSpaces - def setAttributes(self, m21Object): + def setAttributes(self, m21Object: base.Music21Object) -> dict[str, t.Any]: r''' Returns a dictionary and sets self.lilyAttributes to that dictionary, for a m21Object of class classLookup using the mapping of self.m21toLy[classLookup] @@ -135,21 +145,19 @@ def setAttributes(self, m21Object): >>> lilyAttributes is lm.lilyAttributes True ''' - attrs = None - foundClass = False for tryClass in self.supportedClasses: if tryClass in m21Object.classes or tryClass == '*': - attrs = self.setAttributesFromClassObject(tryClass, m21Object) - foundClass = True - break + return self.setAttributesFromClassObject(tryClass, m21Object) - if not foundClass: # pragma: no cover - raise LilyObjectsException( - 'Could not support setting attributes from ' - f'{m21Object}: supported classes: {self.supportedClasses}') - return attrs + raise LilyObjectsException( # pragma: no cover + 'Could not support setting attributes from ' + f'{m21Object}: supported classes: {self.supportedClasses}') - def setAttributesFromClassObject(self, classLookup, m21Object): + def setAttributesFromClassObject( + self, + classLookup: str, + m21Object: base.Music21Object, + ) -> dict[str, t.Any]: r''' Returns a dictionary and sets self.lilyAttributes to that dictionary, for a m21Object of class classLookup using the mapping of self.m21toLy[classLookup] @@ -209,21 +217,24 @@ def _reprInternal(self) -> str: msg = msg[:10] + '...' return msg - def __str__(self): + def __str__(self) -> str: so = self.stringOutput() so = so.replace('\n\n', '\n') return so - def stringOutput(self): + def stringOutput(self) -> str: return '' - def getFirstNonNoneAttribute(self, attributeList): + def getFirstNonNoneAttribute(self, attributeList: t.Iterable[str]) -> t.Any: for a in attributeList: if getattr(self, a) is not None: return getattr(self, a) return None - def newlineSeparateStringOutputIfNotNone(self, contents): + def newlineSeparateStringOutputIfNotNone( + self, + contents: t.Iterable[LyObject|str|None], + ) -> str: c = '' for n in contents: if n is None: @@ -232,7 +243,7 @@ def newlineSeparateStringOutputIfNotNone(self, contents): return c - def encloseCurly(self, arg): + def encloseCurly(self, arg: t.Sequence[str]|LyObject|str|None) -> str: if isinstance(arg, list): strArg = self.newlineIndent.join(arg) return ''.join([' { ', self.newlineIndent, strArg, self.newlineIndent, @@ -243,7 +254,7 @@ def encloseCurly(self, arg): else: return ' { } ' - def quoteString(self, stringIn): + def quoteString(self, stringIn: str) -> str: r''' returns a string that is quoted with internal quotation marks backslash'd out @@ -272,7 +283,7 @@ class LyMock(LyObject): ''' supportedClasses = ['Mock', 'Mocker'] m21toLy = {'Mock': {'mockAttribute': 'mock-attribute', - 'mockAttribute2': 'mock-attribute-2', + 'mockAttribute2': 'mock-attribute-2', }, 'Mocker': {'mockerAttribute': 'mock-attribute', 'greg': 'mock-attribute-2', }, @@ -299,13 +310,13 @@ class LyLilypondTop(LyObject): ''' canContain = [None, 'TopLevelExpression', 'Assignment'] - def __init__(self, contents=None): + def __init__(self, contents: list[LyObject|str]|None = None) -> None: if contents is None: contents = [] super().__init__() self.contents = contents - def stringOutput(self): + def stringOutput(self) -> str: return self.newlineSeparateStringOutputIfNotNone(self.contents) @@ -328,10 +339,16 @@ class LyTopLevelExpression(LyObject): '\\book { } ' ''' - def __init__(self, lilypondHeader=None, bookBlock=None, - bookPartBlock=None, scoreBlock=None, compositeMusic=None, - fullMarkup=None, fullMarkupList=None, outputDef=None - ): + def __init__(self, + lilypondHeader: LyLilypondHeader|None = None, + bookBlock: LyBookBlock|None = None, + bookPartBlock: LyBookpartBlock|None = None, + scoreBlock: LyScoreBlock|None = None, + compositeMusic: LyCompositeMusic|None = None, + fullMarkup: LyFullMarkup|None = None, + fullMarkupList: LyFullMarkupList|None = None, + outputDef: LyOutputDef|None = None, + ) -> None: super().__init__() self.lilypondHeader = lilypondHeader self.bookBlock = bookBlock @@ -342,7 +359,7 @@ def __init__(self, lilypondHeader=None, bookBlock=None, self.fullMarkupList = fullMarkupList self.outputDef = outputDef - def stringOutput(self): + def stringOutput(self) -> str: outputObject = self.getFirstNonNoneAttribute([ 'lilypondHeader', 'bookBlock', 'bookPartBlock', 'scoreBlock', 'compositeMusic', 'fullMarkup', 'fullMarkupList', 'outputDef']) @@ -360,11 +377,11 @@ class LyLilypondHeader(LyObject): '\\header { } ' ''' - def __init__(self, lilypondHeaderBody=None): + def __init__(self, lilypondHeaderBody: LyLilypondHeaderBody|None = None) -> None: super().__init__() self.lilypondHeaderBody = lilypondHeaderBody - def stringOutput(self): + def stringOutput(self) -> str: return self.backslash + 'header' + self.encloseCurly(self.lilypondHeaderBody) @@ -385,22 +402,22 @@ class LyEmbeddedScm(LyObject): '##t' ''' - def __init__(self, content=None): + def __init__(self, content: str = '') -> None: super().__init__() self.content = content - def stringOutput(self): + def stringOutput(self) -> str: return self.content class LyLilypondHeaderBody(LyObject): - def __init__(self, assignments=None): + def __init__(self, assignments: list[LyAssignment]|None = None) -> None: if assignments is None: assignments = [] super().__init__() self.assignments = assignments - def stringOutput(self): + def stringOutput(self) -> str: return self.newlineSeparateStringOutputIfNotNone(self.assignments) @@ -411,12 +428,12 @@ class LyAssignmentId(LyObject): 'title' ''' - def __init__(self, content=None, isLyricString=False): + def __init__(self, content: str = '', isLyricString: bool = False) -> None: super().__init__() self.content = content self.isLyricString = isLyricString - def stringOutput(self): + def stringOutput(self) -> str: return self.content @@ -441,15 +458,19 @@ class LyAssignment(LyObject): but that's overkill for a lot of things. ''' - def __init__(self, assignmentId=None, identifierInit=None, - propertyPath=None, embeddedScm=None): + def __init__(self, + assignmentId: LyAssignmentId|str|None = None, + identifierInit: LyIdentifierInit|None = None, + propertyPath: LyPropertyPath|None = None, + embeddedScm: LyEmbeddedScm|None = None, + ) -> None: super().__init__() self.assignmentId = assignmentId self.identifierInit = identifierInit self.propertyPath = propertyPath self.embeddedScm = embeddedScm - def stringOutput(self): + def stringOutput(self) -> str: if self.embeddedScm is not None: return self.embeddedScm.stringOutput() elif self.propertyPath is not None: @@ -475,14 +496,21 @@ class LyIdentifierInit(LyObject): ''' def __init__(self, - scoreBlock=None, - bookBlock=None, - bookPartBlock=None, - outputDef=None, - contextDefSpecBlock=None, - music=None, postEvent=None, numberExpression=None, - string=None, embeddedScm=None, fullMarkup=None, fullMarkupList=None, - digit=None, contextModification=None): + scoreBlock: LyScoreBlock|None = None, + bookBlock: LyBookBlock|None = None, + bookPartBlock: LyBookpartBlock|None = None, + outputDef: LyOutputDef|None = None, + contextDefSpecBlock: LyContextDefSpecBlock|None = None, + music: LyMusic|None = None, + postEvent: LyPostEvent|None = None, + numberExpression: LyNumberExpression|None = None, + string: str|None = None, + embeddedScm: LyEmbeddedScm|None = None, + fullMarkup: LyFullMarkup|None = None, + fullMarkupList: LyFullMarkupList|None = None, + digit: int|None = None, + contextModification: LyContextModification|None = None, + ) -> None: super().__init__() self.scoreBlock = scoreBlock self.bookBlock = bookBlock @@ -499,7 +527,7 @@ def __init__(self, self.digit = digit self.contextModification = contextModification - def stringOutput(self): + def stringOutput(self) -> str: outputObject = self.getFirstNonNoneAttribute([ 'scoreBlock', 'bookBlock', 'bookPartBlock', 'outputDef', 'contextDefSpecBlock', 'music', 'postEvent', 'numberExpression', @@ -510,18 +538,18 @@ def stringOutput(self): if outputObject is self.digit: # better test for digit return str(outputObject) - elif outputObject is self.string: - return self.quoteString(outputObject) + elif self.string is not None and outputObject is self.string: + return self.quoteString(self.string) else: return outputObject.stringOutput() class LyContextDefSpecBlock(LyObject): - def __init__(self, contextDefSpecBody=None): + def __init__(self, contextDefSpecBody: LyContextDefSpecBody|None = None) -> None: super().__init__() self.contextDefSpecBody = contextDefSpecBody - def stringOutput(self): + def stringOutput(self) -> str: return self.backslash + 'context ' + self.encloseCurly(self.contextDefSpecBody) @@ -545,8 +573,13 @@ class LyContextDefSpecBody(LyObject): 'body \\grobdescriptions #t' ''' - def __init__(self, contextDefIdentifier=None, contextDefSpecBody=None, - embeddedScm=None, contextMod=None, contextModification=None): + def __init__(self, + contextDefIdentifier: str|None = None, + contextDefSpecBody: str|None = None, + embeddedScm: LyEmbeddedScm|None = None, + contextMod: LyContextMod|None = None, + contextModification: LyContextModification|None = None, + ) -> None: super().__init__() self.contextDefIdentifier = contextDefIdentifier self.contextDefSpecBody = contextDefSpecBody @@ -554,7 +587,7 @@ def __init__(self, contextDefIdentifier=None, contextDefSpecBody=None, self.contextMod = contextMod self.contextModification = contextModification - def stringOutput(self): + def stringOutput(self) -> str: if self.contextDefIdentifier is not None: return self.contextDefIdentifier elif self.embeddedScm is not None: @@ -576,15 +609,15 @@ def stringOutput(self): else: return self.contextModification.stringOutput() else: - return None + return '' class LyBookBlock(LyObject): - def __init__(self, bookBody=None): + def __init__(self, bookBody: LyBookBody|None = None) -> None: super().__init__() self.bookBody = bookBody - def stringOutput(self): + def stringOutput(self) -> str: return self.backslash + 'book' + ' ' + self.encloseCurly(self.bookBody) @@ -606,8 +639,8 @@ class LyBookBody(LyObject): 'bookId' >>> lyBookBody = lily.lilyObjects.LyBookBody() - >>> lyBookBody.stringOutput() is None - True + >>> lyBookBody.stringOutput() + '' >>> lyBookBody = lily.lilyObjects.LyBookBody(contents=['a', 'b', 'c']) >>> print(lyBookBody.stringOutput()) @@ -616,18 +649,21 @@ class LyBookBody(LyObject): c ''' - def __init__(self, contents=None, bookIdentifier=None): + def __init__(self, + contents: list[LyObject|str]|None = None, + bookIdentifier: str|None = None, + ) -> None: if contents is None: contents = [] super().__init__() self.contents = contents self.bookIdentifier = bookIdentifier - def stringOutput(self): + def stringOutput(self) -> str: if self.bookIdentifier is not None: return self.bookIdentifier elif not self.contents: - return None + return '' else: return self.newlineSeparateStringOutputIfNotNone(self.contents) @@ -639,11 +675,11 @@ class LyBookpartBlock(LyObject): '\\bookpart { \n\n } \n' ''' - def __init__(self, bookpartBody=None): + def __init__(self, bookpartBody: LyBookpartBody|None = None) -> None: super().__init__() self.bookpartBody = bookpartBody - def stringOutput(self): + def stringOutput(self) -> str: if self.bookpartBody is None: return self.backslash + 'bookpart ' + self.encloseCurly('') else: @@ -668,8 +704,8 @@ class LyBookpartBody(LyObject): 'bookId' >>> lyBookpartBody = lily.lilyObjects.LyBookpartBody() - >>> lyBookpartBody.stringOutput() is None - True + >>> lyBookpartBody.stringOutput() + '' >>> lyBookpartBody = lily.lilyObjects.LyBookpartBody(contents=['a', 'b', 'c']) >>> print(lyBookpartBody.stringOutput()) @@ -678,18 +714,21 @@ class LyBookpartBody(LyObject): c ''' - def __init__(self, contents=None, bookIdentifier=None): + def __init__(self, + contents: list[LyObject|str]|None = None, + bookIdentifier: str|None = None, + ) -> None: if contents is None: contents = [] super().__init__() self.contents = contents self.bookIdentifier = bookIdentifier - def stringOutput(self): + def stringOutput(self) -> str: if self.bookIdentifier is not None: return self.bookIdentifier elif not self.contents: - return None + return '' else: return self.newlineSeparateStringOutputIfNotNone(self.contents) @@ -705,11 +744,11 @@ class LyScoreBlock(LyObject): \score { hello } ''' - def __init__(self, scoreBody=None): + def __init__(self, scoreBody: LyScoreBody|str|None = None) -> None: super().__init__() self.scoreBody = scoreBody - def stringOutput(self): + def stringOutput(self) -> str: if self.scoreBody is None: raise LilyObjectsException('scoreBody object cannot be empty!') # pragma: no cover @@ -729,8 +768,14 @@ class LyScoreBody(LyObject): 'score' ''' - def __init__(self, music=None, scoreIdentifier=None, scoreBody=None, lilypondHeader=None, - outputDef=None, error=None): + def __init__(self, + music: LyMusic|None = None, + scoreIdentifier: str|None = None, + scoreBody: LyScoreBody|None = None, + lilypondHeader: LyLilypondHeader|None = None, + outputDef: LyOutputDef|None = None, + error: LyObject|None = None, + ) -> None: super().__init__() self.music = music self.scoreIdentifier = scoreIdentifier @@ -739,7 +784,7 @@ def __init__(self, music=None, scoreIdentifier=None, scoreBody=None, lilypondHea self.outputDef = outputDef self.error = error - def stringOutput(self): + def stringOutput(self) -> str: if self.music is not None: return self.music.stringOutput() elif self.scoreIdentifier is not None: @@ -761,18 +806,18 @@ def stringOutput(self): class LyPaperBlock(LyObject): - def __init__(self, outputDef=None): + def __init__(self, outputDef: LyOutputDef|None = None) -> None: super().__init__() self.outputDef = outputDef - def stringOutput(self): + def stringOutput(self) -> str: if self.outputDef is None: # legal?? - return None + return '' else: return self.outputDef.stringOutput() class LyLayout(LyObject): - def stringOutput(self): + def stringOutput(self) -> str: theseStrings = [self.backslash + 'layout {', ' ' + self.backslash + 'context {', ' ' + self.backslash + 'RemoveEmptyStaves', @@ -787,11 +832,11 @@ class LyOutputDef(LyObject): This is an ugly grammar, since it does not close the curly bracket. ''' - def __init__(self, outputDefBody=None): + def __init__(self, outputDefBody: LyOutputDefBody|None = None) -> None: super().__init__() self.outputDefBody = outputDefBody - def stringOutput(self): + def stringOutput(self) -> str: if self.outputDefBody is None: raise LilyObjectsException('Need outputDefBody to be set') # pragma: no cover return self.outputDefBody.stringOutput() + '}' @@ -809,11 +854,11 @@ class LyOutputDefHead(LyObject): According to Appendix C, is the same as LyOutputDefHeadWithModeSwitch ''' - def __init__(self, defType=None): + def __init__(self, defType: str|None = None) -> None: super().__init__() self.defType = defType - def stringOutput(self): + def stringOutput(self) -> str: if self.defType not in ('paper', 'midi', 'layout'): # pragma: no cover raise LilyObjectsException("self.defType must be one of 'paper', 'midi', or 'layout'") @@ -832,8 +877,14 @@ class LyOutputDefBody(LyObject): | output_def_body error ''' - def __init__(self, outputDefHead=None, outputDefIdentifier=None, outputDefBody=None, - assignment=None, contextDefSpecBlock=None, error=None): + def __init__(self, + outputDefHead: LyOutputDefHead|None = None, + outputDefIdentifier: str|None = None, + outputDefBody: LyOutputDefBody|None = None, + assignment: LyAssignment|None = None, + contextDefSpecBlock: LyContextDefSpecBlock|None = None, + error: LyObject|None = None, + ) -> None: super().__init__() self.outputDefHead = outputDefHead self.outputDefIdentifier = outputDefIdentifier @@ -842,14 +893,14 @@ def __init__(self, outputDefHead=None, outputDefIdentifier=None, outputDefBody=N self.contextDefSpecBlock = contextDefSpecBlock self.error = error - def stringOutput(self): + def stringOutput(self) -> str: if self.outputDefHead is not None: out = str(self.outputDefHead) + ' { ' if self.outputDefIdentifier is not None: return out + str(self.outputDefIdentifier) else: return out - elif self.outputDefBody is not None: # pragma: no cover + elif self.outputDefBody is None: # pragma: no cover raise LilyObjectsException('Need embedded outputDefBody if outputDefIdentifier ' + 'or outputDefHead are not defined') elif self.assignment is not None: @@ -875,24 +926,28 @@ class LyTempoEvent(LyObject): More complex: - >>> steno = lily.lilyObjects.LyStenoDuration('quarter') + >>> steno = lily.lilyObjects.LyStenoDuration(4) >>> tempoRange = lily.lilyObjects.LyTempoRange(70, 100) >>> lte = lily.lilyObjects.LyTempoEvent(tempoRange=tempoRange, stenoDuration=steno) >>> str(lte) - '\\tempo quarter = 70~100 ' + '\\tempo 4 = 70-100 ' >>> lte.scalar = 85 >>> str(lte) - '\\tempo 85 quarter = 70~100 ' + '\\tempo 85 4 = 70-100 ' ''' - def __init__(self, tempoRange=None, stenoDuration=None, scalar=None): + def __init__(self, + tempoRange: LyTempoRange|None = None, + stenoDuration: LyStenoDuration|None = None, + scalar: int|str|None = None, + ) -> None: super().__init__() self.tempoRange = tempoRange self.stenoDuration = stenoDuration self.scalar = scalar - def stringOutput(self): + def stringOutput(self) -> str: base = self.backslash + 'tempo' if self.tempoRange is not None: if self.stenoDuration is None: # pragma: no cover @@ -917,24 +972,27 @@ class LyMusicList(LyObject): can take any number of LyMusic, LyEmbeddedScm, or LyError objects ''' - def __init__(self, contents=None): + def __init__(self, contents: list[LyObject|str]|None = None) -> None: super().__init__() if contents is None: contents = [] self.contents = contents - def stringOutput(self): + def stringOutput(self) -> str: return self.newlineSeparateStringOutputIfNotNone(self.contents) class LyMusic(LyObject): - def __init__(self, simpleMusic=None, compositeMusic=None): + def __init__(self, + simpleMusic: LySimpleMusic|None = None, + compositeMusic: LyCompositeMusic|None = None, + ) -> None: super().__init__() self.simpleMusic = simpleMusic self.compositeMusic = compositeMusic - def stringOutput(self): + def stringOutput(self) -> str: if self.simpleMusic is not None: return self.simpleMusic.stringOutput() elif self.compositeMusic is not None: @@ -945,11 +1003,11 @@ def stringOutput(self): class LyAlternativeMusic(LyObject): - def __init__(self, musicList=None): + def __init__(self, musicList: LyMusicList|None = None) -> None: super().__init__() self.musicList = musicList - def stringOutput(self): + def stringOutput(self) -> str: if self.musicList is None: return '' else: @@ -958,14 +1016,24 @@ def stringOutput(self): class LyRepeatedMusic(LyObject): - def __init__(self, simpleString=None, unsignedNumber=None, music=None, alternativeMusic=None): + def __init__(self, + simpleString: LyObject|None = None, + unsignedNumber: LyObject|None = None, + music: LyMusic|None = None, + alternativeMusic: LyAlternativeMusic|None = None, + ) -> None: super().__init__() self.simpleString = simpleString self.unsignedNumber = unsignedNumber self.music = music self.alternativeMusic = alternativeMusic - def stringOutput(self): + def stringOutput(self) -> str: + if (self.simpleString is None + or self.unsignedNumber is None + or self.music is None): # pragma: no cover + raise LilyObjectsException( + 'need simpleString, unsignedNumber, and music to output repeated music') out = (self.backslash + 'repeat ' + self.simpleString.stringOutput() @@ -984,13 +1052,17 @@ class LySequentialMusic(LyObject): Can be explicitly tagged with "\sequential" if displayTag is True ''' - def __init__(self, musicList=None, displayTag=False, beforeMatter=None): + def __init__(self, + musicList: LyObject|None = None, + displayTag: bool = False, + beforeMatter: str|None = None, + ) -> None: super().__init__() self.musicList = musicList self.displayTag = displayTag self.beforeMatter = beforeMatter - def stringOutput(self): + def stringOutput(self) -> str: if self.musicList is not None: musicListSO = self.musicList.stringOutput() else: @@ -1015,12 +1087,12 @@ class LyOssiaMusic(LyObject): Can be tagged with \startStaff and \stopStaff if startstop is True ''' - def __init__(self, musicList=None, startstop=True): + def __init__(self, musicList: LyMusicList|None = None, startstop: bool = True) -> None: super().__init__() self.musicList = musicList self.startstop = startstop - def stringOutput(self): + def stringOutput(self) -> str: if self.startstop is True: start = self.backslash + 'startStaff ' stop = self.backslash + 'stopStaff' @@ -1043,12 +1115,12 @@ class LySimultaneousMusic(LyObject): otherwise encloses in double angle brackets ''' - def __init__(self, musicList=None, displayTag=False): + def __init__(self, musicList: LyMusicList|None = None, displayTag: bool = False) -> None: super().__init__() self.musicList = musicList self.displayTag = displayTag - def stringOutput(self): + def stringOutput(self) -> str: if self.musicList is not None: musicListSO = self.musicList.stringOutput() else: @@ -1062,15 +1134,19 @@ def stringOutput(self): class LySimpleMusic(LyObject): - def __init__(self, eventChord=None, musicIdentifier=None, - musicPropertyDef=None, contextChange=None): + def __init__(self, + eventChord: LyEventChord|None = None, + musicIdentifier: LyObject|None = None, + musicPropertyDef: LyMusicPropertyDef|None = None, + contextChange: LyContextChange|None = None, + ) -> None: super().__init__() self.eventChord = eventChord self.musicIdentifier = musicIdentifier self.musicPropertyDef = musicPropertyDef self.contextChange = contextChange - def stringOutput(self): + def stringOutput(self) -> str: outputObject = self.getFirstNonNoneAttribute(['eventChord', 'musicIdentifier', 'musicPropertyDef', 'contextChange']) if outputObject is None: @@ -1085,13 +1161,17 @@ class LyContextModification(LyObject): but not context_mod!!!!! ''' - def __init__(self, contextModList=None, contextModIdentifier=None, displayWith=True): + def __init__(self, + contextModList: LyContextModList|list[str]|None = None, + contextModIdentifier: str|None = None, + displayWith: bool = True, + ) -> None: super().__init__() self.contextModList = contextModList - self.contextModIdentifier = contextModIdentifier # String? + self.contextModIdentifier = contextModIdentifier self.displayWith = displayWith # optional, but not supported without so far - def stringOutput(self): + def stringOutput(self) -> str: if self.contextModList is not None: return self.backslash + 'with ' + self.encloseCurly(self.contextModList) elif self.contextModIdentifier is not None: @@ -1105,14 +1185,17 @@ class LyContextModList(LyObject): contains zero or more LyContextMod objects and an optional contextModIdentifier ''' - def __init__(self, contents=None, contextModIdentifier=None): + def __init__(self, + contents: list[LyContextMod]|None = None, + contextModIdentifier: str|None = None, + ) -> None: if contents is None: contents = [] super().__init__() self.contents = contents - self.contextModIdentifier = contextModIdentifier # STRING + self.contextModIdentifier = contextModIdentifier - def stringOutput(self): + def stringOutput(self) -> str: output = self.newlineSeparateStringOutputIfNotNone(self.contents) if self.contextModIdentifier is not None: return output + ' ' + self.contextModIdentifier @@ -1125,20 +1208,25 @@ class LyCompositeMusic(LyObject): one of LyPrefixCompositeMusic or LyGroupedMusicList stored in self.contents ''' - def __init__(self, prefixCompositeMusic=None, groupedMusicList=None, newLyrics=None): + def __init__(self, + prefixCompositeMusic: LyPrefixCompositeMusic|None = None, + groupedMusicList: LyObject|None = None, + newLyrics: LyNewLyrics|None = None, + ) -> None: super().__init__() self.prefixCompositeMusic = prefixCompositeMusic self.groupedMusicList = groupedMusicList self.newLyrics = newLyrics @property - def contents(self): + def contents(self) -> LyObject|None: if self.prefixCompositeMusic is not None: return self.prefixCompositeMusic else: return self.groupedMusicList - def stringOutput(self): + def stringOutput(self) -> str: + newLyrics: LyNewLyrics|str if self.newLyrics is not None: newLyrics = self.newLyrics else: @@ -1158,12 +1246,15 @@ class LyGroupedMusicList(LyObject): one of LySimultaneousMusic or LySequentialMusic ''' - def __init__(self, simultaneousMusic=None, sequentialMusic=None): + def __init__(self, + simultaneousMusic: LySimultaneousMusic|None = None, + sequentialMusic: LySequentialMusic|None = None, + ) -> None: super().__init__() self.simultaneousMusic = simultaneousMusic self.sequentialMusic = sequentialMusic - def stringOutput(self): + def stringOutput(self) -> str: if self.simultaneousMusic is not None: return str(self.simultaneousMusic) elif self.sequentialMusic is not None: @@ -1212,13 +1303,13 @@ class LySchemeFunction(LyObject): We have usually been using LyEmbeddedScm for this ''' - def __init__(self, content=None): + def __init__(self, content: LyObject|str|None = None) -> None: super().__init__() self.content = content - def stringOutput(self): + def stringOutput(self) -> str: if self.content is None: - return None + return '' else: return str(self.content) @@ -1228,13 +1319,13 @@ class LyOptionalId(LyObject): an optional id setting ''' - def __init__(self, content=None): + def __init__(self, content: str|None = None) -> None: super().__init__() self.content = content - def stringOutput(self): + def stringOutput(self) -> str: if self.content is None: - return None + return '' else: return ' = ' + self.content @@ -1272,14 +1363,23 @@ class LyPrefixCompositeMusic(LyObject): | re_rhythmed_music ''' # pylint: disable=redefined-builtin - def __init__(self, type=None, genericPrefixMusicScm=None, - simpleString=None, optionalId=None, optionalContextMod=None, - music=None, fraction=None, repeatedMusic=None, - pitchAlsoInChords1=None, pitchAlsoInChords2=None, - modeChangingHead=None, groupedMusicList=None, - modeChangingHeadWithContext=None, relativeMusic=None, - reRhythmedMusic=None - ): + def __init__(self, + type: str|None = None, + genericPrefixMusicScm: LyObject|None = None, + simpleString: str|None = None, + optionalId: LyOptionalId|None = None, + optionalContextMod: LyContextModification|None = None, + music: LyObject|str|None = None, + fraction: str|None = None, + repeatedMusic: LyRepeatedMusic|None = None, + pitchAlsoInChords1: LyPitch|None = None, + pitchAlsoInChords2: LyPitch|None = None, + modeChangingHead: LyModeChangingHead|None = None, + groupedMusicList: LyGroupedMusicList|None = None, + modeChangingHeadWithContext: LyModeChangingHead|None = None, + relativeMusic: LyRelativeMusic|None = None, + reRhythmedMusic: LyReRhythmedMusic|None = None, + ) -> None: super().__init__() self.type = type self.genericPrefixMusicScm = genericPrefixMusicScm @@ -1297,7 +1397,7 @@ def __init__(self, type=None, genericPrefixMusicScm=None, self.relativeMusic = relativeMusic self.reRhythmedMusic = reRhythmedMusic - def stringOutput(self): + def stringOutput(self) -> str: myType = self.type if myType == 'scheme': return str(self.genericPrefixMusicScm) @@ -1346,24 +1446,30 @@ class LyModeChangingHead(LyObject): >>> print(l2.stringOutput()) \chords + 'note' has no context-creating shorthand in LilyPond, so it always gives `\notemode`: + + >>> l3 = lily.lilyObjects.LyModeChangingHead(hasContext=False, mode='note') + >>> print(l3.stringOutput()) + \notemode ''' allowableModes = ['note', 'drum', 'figure', 'chord', 'lyric'] - def __init__(self, hasContext=False, mode=None): + def __init__(self, hasContext: bool = False, mode: str|None = None) -> None: super().__init__() self.hasContext = hasContext self.mode = mode - def stringOutput(self): - if self.mode is None: + def stringOutput(self) -> str: + mode = self.mode + if mode is None: raise LilyObjectsException('Mode must be set') # pragma: no cover - if self.mode not in self.allowableModes: - raise LilyObjectsException(f'Not an allowable mode {self.mode}') # pragma: no cover + if mode not in self.allowableModes: + raise LilyObjectsException(f'Not an allowable mode {mode}') # pragma: no cover - if self.hasContext: - return self.backslash + self.mode + 'mode' + if self.hasContext or mode == 'note': + return self.backslash + mode + 'mode' else: - return self.backslash + self.mode + 's' + return self.backslash + mode + 's' class LyRelativeMusic(LyObject): @@ -1371,11 +1477,13 @@ class LyRelativeMusic(LyObject): relative music ''' - def __init__(self, content=None): + def __init__(self, content: LyObject|None = None) -> None: super().__init__() self.content = content - def stringOutput(self): + def stringOutput(self) -> str: + if self.content is None: # pragma: no cover + raise LilyObjectsException('need content for relative music') return self.backslash + 'relative ' + self.content.stringOutput() @@ -1384,17 +1492,17 @@ class LyNewLyrics(LyObject): contains a list of LyGroupedMusicList objects or identifiers ''' - def __init__(self, groupedMusicLists=None): + def __init__(self, groupedMusicLists: list[LyGroupedMusicList|str]|None = None) -> None: if groupedMusicLists is None: groupedMusicLists = [] super().__init__() self.groupedMusicLists = groupedMusicLists - def stringOutput(self): + def stringOutput(self) -> str: outputString = '' for c in self.groupedMusicLists: outputString += self.backslash + 'addlyrics ' - if hasattr(c, 'stringOutput'): + if isinstance(c, LyObject): outputString += c.stringOutput() else: outputString += c + ' ' @@ -1403,19 +1511,25 @@ def stringOutput(self): class LyReRhythmedMusic(LyObject): - def __init__(self, groupedMusic=None, newLyrics=None): + def __init__(self, + groupedMusic: LyGroupedMusicList|str|None = None, + newLyrics: LyNewLyrics|None = None, + ) -> None: super().__init__() self.groupedMusic = groupedMusic self.newLyrics = newLyrics - def stringOutput(self): + def stringOutput(self) -> str: c = self.groupedMusic - if hasattr(c, 'stringOutput'): + if isinstance(c, LyObject): outputString = c.stringOutput() - else: + elif c is not None: outputString = c + ' ' - outputString += self.newLyrics.stringOutput() - return outputString # previously this did not return + else: # pragma: no cover + raise LilyObjectsException('need groupedMusic for re-rhythmed music') + if self.newLyrics is not None: + outputString += self.newLyrics.stringOutput() + return outputString class LyContextChange(LyObject): @@ -1425,12 +1539,12 @@ class LyContextChange(LyObject): '\\change x = y ' ''' - def __init__(self, before=None, after=None): + def __init__(self, before: str = '', after: str = '') -> None: super().__init__() self.before = before self.after = after - def stringOutput(self): + def stringOutput(self) -> str: return self.backslash + 'change ' + self.before + ' = ' + self.after + ' ' @@ -1441,14 +1555,14 @@ class LyPropertyPath(LyObject): has one or more of LyEmbeddedScm objects ''' - def __init__(self, embeddedScheme=None): + def __init__(self, embeddedScheme: list[LyEmbeddedScm]|None = None) -> None: if embeddedScheme is None: embeddedScheme = [] super().__init__() self.embeddedScheme = embeddedScheme - def stringOutput(self): + def stringOutput(self) -> str: return ' '.join([es.stringOutput() for es in self.embeddedScheme]) @@ -1480,17 +1594,19 @@ class LyPropertyOperation(LyObject): TODO: should \set be given? ''' - def __init__(self, mode=None, value1=None, value2=None, value3=None): + def __init__(self, + mode: str|None = None, + value1: str = '', + value2: str = '', + value3: str = '', + ) -> None: super().__init__() self.mode = mode self.value1 = value1 self.value2 = value2 self.value3 = value3 - def stringOutput(self): - if self.mode not in ('set', 'unset', 'override', 'revert'): - raise LilyObjectsException(f'invalid mode {self.mode}') - + def stringOutput(self) -> str: if self.mode == 'set': return self.backslash + 'set ' + self.value1 + ' = ' + self.value2 + ' ' elif self.mode == 'unset': @@ -1500,6 +1616,8 @@ def stringOutput(self): ' = ', self.value3, ' ']) elif self.mode == 'revert': return self.backslash + 'revert ' + self.value1 + '.' + self.value2 + ' ' + else: + raise LilyObjectsException(f'invalid mode {self.mode}') class LyContextDefMod(LyObject): @@ -1507,21 +1625,26 @@ class LyContextDefMod(LyObject): one of consists, remove, accepts, defaultchild, denies, alias, type, description, name ''' - def __init__(self, contextDef=None): + def __init__(self, contextDef: str = '') -> None: super().__init__() self.contextDef = contextDef - def stringOutput(self): + def stringOutput(self) -> str: return self.backslash + self.contextDef + ' ' class LyContextMod(LyObject): - def __init__(self, contextDefOrProperty=None, scalar=None): + def __init__(self, + contextDefOrProperty: LyContextDefMod|LyPropertyOperation|None = None, + scalar: str|None = None, + ) -> None: super().__init__() self.contextDefOrProperty = contextDefOrProperty self.scalar = scalar - def stringOutput(self): + def stringOutput(self) -> str: + if self.contextDefOrProperty is None: # pragma: no cover + raise LilyObjectsException('need a contextDef or property to modify a context') if self.scalar is None: return self.contextDefOrProperty.stringOutput() else: @@ -1533,12 +1656,14 @@ def stringOutput(self): class LyMusicPropertyDef(LyObject): - def __init__(self, isOnce=False, propertyDef=None): + def __init__(self, isOnce: bool = False, propertyDef: LyPropertyOperation|None = None) -> None: super().__init__() self.isOnce = isOnce self.propertyDef = propertyDef - def stringOutput(self): + def stringOutput(self) -> str: + if self.propertyDef is None: # pragma: no cover + raise LilyObjectsException('need a propertyDef for a music property definition') s = '' if self.isOnce: s += self.backslash + 'once ' @@ -1562,8 +1687,15 @@ class LyEventChord(LyObject): once that is done. But there is no LySimpleChordElements object yet. ''' - def __init__(self, simpleChordElements=None, postEvents=None, chordRepetition=None, - multiMeasureRest=None, duration=None, commandElement=None, noteChordElement=None): + def __init__(self, + simpleChordElements: LySimpleElement|None = None, + postEvents: list[LyObject|str]|None = None, + chordRepetition: LyObject|str|None = None, + multiMeasureRest: LyObject|str|None = None, + duration: str|None = None, + commandElement: LyCommandElement|None = None, + noteChordElement: LyNoteChordElement|None = None, + ) -> None: super().__init__() self.simpleChordElements = simpleChordElements self.postEvents = postEvents @@ -1573,7 +1705,7 @@ def __init__(self, simpleChordElements=None, postEvents=None, chordRepetition=No self.commandElement = commandElement self.noteChordElement = noteChordElement - def stringOutput(self): + def stringOutput(self) -> str: if self.noteChordElement is not None: return str(self.noteChordElement) + ' ' elif self.commandElement is not None: @@ -1606,7 +1738,11 @@ def stringOutput(self): class LyNoteChordElement(LyObject): - def __init__(self, chordBody=None, optionalNoteModeDuration=None, postEvents=None): + def __init__(self, + chordBody: LyChordBody|LyPitch|None = None, + optionalNoteModeDuration: LyMultipliedDuration|None = None, + postEvents: list[LyObject|str]|None = None, + ) -> None: if postEvents is None: postEvents = [] super().__init__() @@ -1614,7 +1750,7 @@ def __init__(self, chordBody=None, optionalNoteModeDuration=None, postEvents=Non self.optionalNoteModeDuration = optionalNoteModeDuration self.postEvents = postEvents - def stringOutput(self): + def stringOutput(self) -> str: c = str(self.chordBody) if self.optionalNoteModeDuration is not None: c += str(self.optionalNoteModeDuration) + ' ' @@ -1625,14 +1761,14 @@ def stringOutput(self): class LyChordBody(LyObject): - def __init__(self, chordBodyElements=None): + def __init__(self, chordBodyElements: list[LyChordBodyElement]|None = None) -> None: if chordBodyElements is None: chordBodyElements = [] super().__init__() self.chordBodyElements = chordBodyElements - def stringOutput(self): + def stringOutput(self) -> str: c = ' '.join([str(cbe) for cbe in self.chordBodyElements]) return ' '.join(['<', c, '> ']) @@ -1652,13 +1788,13 @@ class LyChordBodyElement(LyObject): TODO: only the first form is currently supported in creation ''' - def __init__(self, parts=None): + def __init__(self, parts: list[LyObject|str]|None = None) -> None: if parts is None: parts = [] super().__init__() self.parts = parts - def stringOutput(self): + def stringOutput(self) -> str: return ' '.join([str(p) for p in self.parts]) # music_function_identifier_musicless_prefix: MUSIC_FUNCTION @@ -1680,41 +1816,53 @@ def stringOutput(self): class LyCommandElement(LyObject): - def __init__(self, commandType=None, argument=None): + def __init__(self, + commandType: LyObject|str|None = None, + argument: LyObject|str|None = None, + ) -> None: super().__init__() self.commandType = commandType self.argument = argument - def stringOutput(self): + def stringOutput(self) -> str: ct = self.commandType - if ct == 'skip': - return self.backslash + 'skip ' + self.argument.stringOutput() + if not isinstance(ct, str): + if ct is None: # pragma: no cover + raise LilyObjectsException('need a commandType to output a command element') + return ct.stringOutput() + + arg = self.argument + if ct in ('skip', 'partial'): + argOut = arg.stringOutput() if isinstance(arg, LyObject) else str(arg) + return self.backslash + ct + ' ' + argOut elif ct == '[': + # \[ and \] are ligature brackets; manual beams are plain [ and ] return self.backslash + '[ ' elif ct == ']': return self.backslash + '] ' elif ct == self.backslash: return ct + ' ' - elif ct == 'partial': - return self.backslash + 'partial ' + self.argument.stringOutput() - elif ct == 'time': - return self.backslash + 'time ' + self.argument + ' ' - elif ct == 'mark': - return self.backslash + 'mark ' + self.argument + ' ' - else: - return ct.stringOutput() + elif ct in ('time', 'mark'): + return self.backslash + ct + ' ' + str(arg) + ' ' + else: # pragma: no cover + raise LilyObjectsException(f'unknown commandType {ct}') class LyCommandEvent(LyObject): - def __init__(self, commandType=None, argument1=None, argument2=None): + def __init__(self, + commandType: LyTempoEvent|str|None = None, + argument1: str = '', + argument2: str = '', + ) -> None: super().__init__() self.commandType = commandType self.argument1 = argument1 self.argument2 = argument2 - def stringOutput(self): + def stringOutput(self) -> str: ct = self.commandType - if ct == '~': # ??? not tie? + if ct == '~': + # E_TILDE: the pes-or-flexa ligature event, not a tie (a tie is a bare ~) return self.backslash + '~ ' elif ct == 'mark-default': return self.backslash + 'mark ' + self.backslash + 'default ' @@ -1723,29 +1871,31 @@ def stringOutput(self): elif ct == 'key': # \key NOTENAME_PITCH SCM_IDENTIFIER return self.backslash + 'key ' + self.argument1 + ' ' + self.argument2 + ' ' - else: # tempo_event + elif isinstance(ct, LyTempoEvent): return ct.stringOutput() + else: # pragma: no cover + raise LilyObjectsException(f'unknown commandType {ct}') class LyPostEvents(LyObject): - def __init__(self, eventList=None): + def __init__(self, eventList: list[LyObject]|None = None) -> None: if eventList is None: eventList = [] super().__init__() self.eventList = eventList - def stringOutput(self): + def stringOutput(self) -> str: return ' '.join([e.stringOutput() for e in self.eventList]) class LyPostEvent(LyObject): - def __init__(self, arg1=None, arg2=None): + def __init__(self, arg1: LyObject|str|None = None, arg2: LyObject|str|None = None) -> None: super().__init__() self.arg1 = arg1 self.arg2 = arg2 - def stringOutput(self): + def stringOutput(self) -> str: c = str(self.arg1) if self.arg2 is not None: c += ' ' + str(self.arg2) @@ -1758,34 +1908,34 @@ class LyDirectionLessEvent(LyObject): or an EVENT_IDENTIFIER or a tremolo_type ''' - def __init__(self, event=None): + def __init__(self, event: LyObject|str|None = None) -> None: super().__init__() self.event = event - def stringOutput(self): + def stringOutput(self) -> str: return str(self.event) + ' ' # noinspection SpellCheckingInspection class LyDirectionReqdEvent(LyObject): - def __init__(self, event=None): + def __init__(self, event: LyObject|str|None = None) -> None: super().__init__() self.event = event - def stringOutput(self): + def stringOutput(self) -> str: return str(self.event) + ' ' class LyOctaveCheck(LyObject): - def __init__(self, equalOrQuotesOrNone=None): + def __init__(self, equalOrQuotesOrNone: str|None = None) -> None: super().__init__() self.equalOrQuotesOrNone = equalOrQuotesOrNone - def stringOutput(self): + def stringOutput(self) -> str: eqn = self.equalOrQuotesOrNone if eqn is None: - return None + return '' elif eqn == '=': return '= ' else: @@ -1798,13 +1948,13 @@ class LyPitch(LyObject): also used for steno_pitch and steno_tonic_pitch ''' - def __init__(self, noteNamePitch=None, quotes=None): + def __init__(self, noteNamePitch: str = '', quotes: str = '') -> None: super().__init__() self.noteNamePitch = noteNamePitch self.quotes = quotes - def stringOutput(self): - return self.noteNamePitch + str(self.quotes) + ' ' + def stringOutput(self) -> str: + return self.noteNamePitch + self.quotes + ' ' # no need for pitch_also_in_chords @@ -1814,11 +1964,11 @@ class LyGenTextDef(LyObject): holds either full_markup, string, or DIGIT ''' - def __init__(self, value=None): + def __init__(self, value: LyFullMarkup|str|int|None = None) -> None: super().__init__() self.value = value - def stringOutput(self): + def stringOutput(self) -> str: return str(self.value) + ' ' @@ -1830,12 +1980,12 @@ class LyScriptAbbreviation(LyObject): ''' - def __init__(self, value=None): + def __init__(self, value: str = '') -> None: super().__init__() self.value = value - def stringOutput(self): - return str(self.value) + ' ' + def stringOutput(self) -> str: + return self.value + ' ' class LyScriptDir(LyObject): @@ -1846,12 +1996,12 @@ class LyScriptDir(LyObject): ''' - def __init__(self, value=None): + def __init__(self, value: str = '') -> None: super().__init__() self.value = value - def stringOutput(self): - return str(self.value) + ' ' + def stringOutput(self) -> str: + return self.value + ' ' # no need for absolute_pitch # no need for optional_notemode_duration -- we can use LyMultipliedDuration or None @@ -1869,12 +2019,12 @@ class LyStenoDuration(LyObject): ''' - def __init__(self, durationNumber=None, numDots=0): + def __init__(self, durationNumber: int|str|None = None, numDots: int = 0) -> None: super().__init__() self.durationNumber = durationNumber self.numDots = numDots - def stringOutput(self): + def stringOutput(self) -> str: dotStr = '.' * self.numDots return str(self.durationNumber) + dotStr + ' ' @@ -1884,20 +2034,23 @@ class LyMultipliedDuration(LyObject): represents either a simple LyStenoDuration or a list of things that the steno duration should be multiplied by. - if stenoDur is None then output is None -- thus also represents + if stenoDur is None then output is empty -- thus also represents optional_notemode_duration ''' - def __init__(self, stenoDur=None, multiply=None): + def __init__(self, + stenoDur: LyStenoDuration|None = None, + multiply: list[int|str]|None = None, + ) -> None: if multiply is None: multiply = [] super().__init__() self.stenoDur = stenoDur self.multiply = multiply - def stringOutput(self): + def stringOutput(self) -> str: if self.stenoDur is None: - return None + return '' else: s = str(self.stenoDur) for m in self.multiply: @@ -1907,11 +2060,11 @@ def stringOutput(self): class LyTremoloType(LyObject): - def __init__(self, tremTypeOrNone=None): + def __init__(self, tremTypeOrNone: int|str|None = None) -> None: super().__init__() self.tremTypeOrNone = tremTypeOrNone - def stringOutput(self): + def stringOutput(self) -> str: if self.tremTypeOrNone is not None: return ':' + str(self.tremTypeOrNone) + ' ' else: @@ -1921,11 +2074,11 @@ def stringOutput(self): class LyOptionalRest(LyObject): - def __init__(self, rest=False): + def __init__(self, rest: bool = False) -> None: super().__init__() self.rest = rest - def stringOutput(self): + def stringOutput(self) -> str: if self.rest is False: return '' else: @@ -1947,13 +2100,13 @@ class LySimpleElement(LyObject): | lyric_element optional_notemode_duration ''' - def __init__(self, parts=None): + def __init__(self, parts: list[LyObject|str]|None = None) -> None: if parts is None: parts = [] super().__init__() self.parts = parts - def stringOutput(self): + def stringOutput(self) -> str: return ''.join([str(p) for p in self.parts]) # SKIPPING ALL ChordSymbol Markup for now @@ -1970,29 +2123,35 @@ class LyLyricElement(LyObject): hel_ ''' - def __init__(self, lyMarkupOrString=None): + def __init__(self, lyMarkupOrString: LyMarkup|str|None = None) -> None: super().__init__() self.lyMarkupOrString = lyMarkupOrString - def stringOutput(self): + def stringOutput(self) -> str: return str(self.lyMarkupOrString) + ' ' class LyTempoRange(LyObject): r''' defines either a single tempo or a range + + >>> print(lily.lilyObjects.LyTempoRange(70, 100)) + 70-100 ''' - def __init__(self, lowestOrOnlyTempo=None, highestTempoOrNone=None): + def __init__(self, + lowestOrOnlyTempo: int|str|None = None, + highestTempoOrNone: int|str|None = None, + ) -> None: super().__init__() self.lowestOrOnlyTempo = lowestOrOnlyTempo self.highestTempoOrNone = highestTempoOrNone - def stringOutput(self): + def stringOutput(self) -> str: if self.highestTempoOrNone is None: return str(self.lowestOrOnlyTempo) + ' ' else: - return str(self.lowestOrOnlyTempo) + '~' + str(self.highestTempoOrNone) + ' ' + return str(self.lowestOrOnlyTempo) + '-' + str(self.highestTempoOrNone) + ' ' class LyNumberExpression(LyObject): @@ -2000,13 +2159,13 @@ class LyNumberExpression(LyObject): any list of numbers or LyNumberTerms separated by '+' or '-' objects. ''' - def __init__(self, numberAndSepList=None): + def __init__(self, numberAndSepList: list[LyNumberTerm|int|str]|None = None) -> None: if numberAndSepList is None: numberAndSepList = [] super().__init__() self.numberAndSepList = numberAndSepList - def stringOutput(self): + def stringOutput(self) -> str: c = ' '.join([str(s) for s in self.numberAndSepList]) return c + ' ' @@ -2016,24 +2175,27 @@ class LyNumberTerm(LyObject): any list of numbers separated by '*' or '/' strings. ''' - def __init__(self, numberAndSepList=None): + def __init__(self, numberAndSepList: list[int|str]|None = None) -> None: if numberAndSepList is None: numberAndSepList = [] super().__init__() self.numberAndSepList = numberAndSepList - def stringOutput(self): + def stringOutput(self) -> str: c = ' '.join([str(s) for s in self.numberAndSepList]) return c + ' ' class LyLyricMarkup(LyObject): - def __init__(self, lyricMarkupOrIdentifier=None, markupTop=None): + def __init__(self, + lyricMarkupOrIdentifier: LyObject|str|None = None, + markupTop: LyMarkupTop|None = None, + ) -> None: super().__init__() self.lyricMarkupOrIdentifier = lyricMarkupOrIdentifier self.markupTop = markupTop - def stringOutput(self): + def stringOutput(self) -> str: if self.markupTop is None: return str(self.lyricMarkupOrIdentifier) + ' ' else: @@ -2041,81 +2203,92 @@ def stringOutput(self): class LyFullMarkupList(LyObject): - def __init__(self, markupListOrIdentifier=None): + r''' + >>> markupList = lily.lilyObjects.LyMarkupList('{ "a" "b" }') + >>> print(lily.lilyObjects.LyFullMarkupList(markupList)) + \markuplist { "a" "b" } + ''' + + def __init__(self, markupListOrIdentifier: LyMarkupList|str|None = None) -> None: super().__init__() self.markupListOrIdentifier = markupListOrIdentifier - def stringOutput(self): - if isinstance(self.markupListOrIdentifier, str): - return self.markupListOrIdentifier + ' ' + def stringOutput(self) -> str: + mli = self.markupListOrIdentifier + if isinstance(mli, str): + return mli + ' ' + elif mli is None: # pragma: no cover + raise LilyObjectsException('need a markup list or identifier') else: - # noinspection SpellCheckingInspection - return self.backslash + 'markuplines ' + self.markupListOrIdentifier.stringOutput() + # \markuplines was renamed \markuplist in LilyPond 2.16 + return self.backslash + 'markuplist ' + mli.stringOutput() class LyFullMarkup(LyObject): - def __init__(self, markupTopOrIdentifier=None): + def __init__(self, markupTopOrIdentifier: LyMarkupTop|str|None = None) -> None: super().__init__() self.markupTopOrIdentifier = markupTopOrIdentifier - def stringOutput(self): - if isinstance(self.markupTopOrIdentifier, str): - return self.markupTopOrIdentifier + ' ' + def stringOutput(self) -> str: + mti = self.markupTopOrIdentifier + if isinstance(mti, str): + return mti + ' ' + elif mti is None: # pragma: no cover + raise LilyObjectsException('need a markup top or identifier') else: - return self.backslash + 'markup ' + self.markupTopOrIdentifier.stringOutput() + return self.backslash + 'markup ' + mti.stringOutput() class LyMarkupTop(LyObject): - def __init__(self, argument1=None, argument2=None): + def __init__(self, argument1: str = '', argument2: str|None = None) -> None: super().__init__() self.argument1 = argument1 self.argument2 = argument2 - def stringOutput(self): + def stringOutput(self) -> str: if self.argument2 is None: - return str(self.argument1) + return self.argument1 else: return ' '.join([self.argument1, self.argument2]) class LyMarkupList(LyObject): - def __init__(self, markupIdentifierOrList=None): + def __init__(self, markupIdentifierOrList: LyObject|str|None = None) -> None: super().__init__() self.markupIdentifierOrList = markupIdentifierOrList - def stringOutput(self): + def stringOutput(self) -> str: return str(self.markupIdentifierOrList) class LyMarkupComposedList(LyObject): - def __init__(self, markupHeadList=None, markupBracedList=None): + def __init__(self, markupHeadList: str = '', markupBracedList: str = '') -> None: super().__init__() self.markupHeadList = markupHeadList self.markupBracedList = markupBracedList - def stringOutput(self): + def stringOutput(self) -> str: return ' '.join([self.markupHeadList, self.markupBracedList]) class LyMarkupBracedList(LyObject): - def __init__(self, listBody=None): + def __init__(self, listBody: str = '') -> None: super().__init__() self.listBody = listBody - def stringOutput(self): + def stringOutput(self) -> str: return ' '.join(['{', self.listBody, '}']) class LyMarkupBracedListBody(LyObject): - def __init__(self, markupOrMarkupList=None): + def __init__(self, markupOrMarkupList: list[LyObject|str]|None = None) -> None: if markupOrMarkupList is None: markupOrMarkupList = [] super().__init__() self.markupOrMarkupList = markupOrMarkupList - def stringOutput(self): - super().__init__() + def stringOutput(self) -> str: c = '' for m in self.markupOrMarkupList: c += str(m) + ' ' @@ -2136,28 +2309,37 @@ class LySimpleMarkup(LyObject): takes 1 required arg, 2nd for markup_function ''' - def __init__(self, simpleType='string', argument1=None, argument2=None): + def __init__(self, + simpleType: str = 'string', + argument1: str = '', + argument2: LyObject|str|None = None, + ) -> None: super().__init__() self.simpleType = simpleType self.argument1 = argument1 self.argument2 = argument2 - def stringOutput(self): + def stringOutput(self) -> str: if self.simpleType == 'string': return self.argument1 + ' ' elif self.simpleType == 'score-body': return self.backslash + 'score { ' + self.argument1 + ' } ' elif self.simpleType == 'markup-function': return self.argument1 + ' ' + str(self.argument2) + ' ' + else: # pragma: no cover + raise LilyObjectsException(f'unknown simpleType {self.simpleType}') class LyMarkup(LyObject): - def __init__(self, simpleMarkup=None, optionalMarkupHeadList=None): + def __init__(self, + simpleMarkup: LySimpleMarkup|None = None, + optionalMarkupHeadList: str|None = None, + ) -> None: super().__init__() self.simpleMarkup = simpleMarkup self.optionalMarkupHeadList = optionalMarkupHeadList - def stringOutput(self): + def stringOutput(self) -> str: if self.optionalMarkupHeadList is not None: c = self.optionalMarkupHeadList + ' ' else: diff --git a/music21/lily/translate.py b/music21/lily/translate.py index 2975dcb20..cd7f1e4e2 100644 --- a/music21/lily/translate.py +++ b/music21/lily/translate.py @@ -21,8 +21,11 @@ import re import subprocess import sys +import typing as t import unittest +from music21 import base +from music21 import chord from music21 import clef from music21 import common from music21.converter.subConverters import SubConverter @@ -31,7 +34,11 @@ from music21 import environment from music21 import exceptions21 from music21 import key +from music21 import layout +from music21 import metadata +from music21 import meter from music21 import note +from music21 import pitch from music21 import stream from music21 import variant @@ -58,7 +65,7 @@ class _sharedCorpusTestObject: sharedCacheObject = _sharedCorpusTestObject() -def _getCachedCorpusFile(keyName): +def _getCachedCorpusFile(keyName: str) -> stream.Stream: # return corpus.parse(keyName) if keyName not in sharedCacheObject.sharedCache: sharedCacheObject.sharedCache[keyName] = corpus.parse(keyName) @@ -67,7 +74,7 @@ def _getCachedCorpusFile(keyName): # b.parts[0].measure(4)[2].color = 'blue'#.rightBarline = 'double' -def makeLettersOnlyId(inputString): +def makeLettersOnlyId(inputString: str|int) -> str: # noinspection SpellCheckingInspection r''' Takes an id and makes it purely letters by substituting @@ -133,46 +140,48 @@ class LilypondConverter: 'half-flat': 'eh', } + # bar line names as defined by LilyPond 2.18 and later; unknown names + # are silently drawn as nothing. barlineDict = {'regular': '|', - 'dotted': ':', - 'dashed': 'dashed', - 'heavy': '.', # ?? + 'dotted': ';', + 'dashed': '!', + 'heavy': '.', 'double': '||', 'final': '|.', 'heavy-light': '.|', - 'heavy-heavy': '.|.', - 'start-repeat': '|:', - 'end-repeat': ':|', + 'heavy-heavy': '..', + 'start-repeat': '.|:', + 'end-repeat': ':|.', # no music21 support for |.| lightHeavyLight yet 'tick': "'", - # 'short': '', # no lilypond support?? + 'short': ',', 'none': '', } - def __init__(self): + def __init__(self) -> None: self.majorVersion = '1' self.minorVersion = '0' self.versionString = '1.0' self.backend = 'ps' - self.versionScheme = '' - self.headerScheme = '' + self.versionScheme = lyo.LyEmbeddedScm() + self.headerScheme = lyo.LyEmbeddedScm() self.backendString = '--backend=' + self.LILYEXEC = '' self.topLevelObject = lyo.LyLilypondTop() self.setupTools() - self.context = self.topLevelObject - self.storedContexts = [] - self.doNotOutput = [] - self.currentMeasure = None - self.addedVariants = [] + self.context: lyo.LyObject = self.topLevelObject + self.storedContexts: list[lyo.LyObject] = [] + self.doNotOutput: list[base.Music21Object] = [] + self.currentMeasure: stream.Measure|None = None + self.addedVariants: list[str] = [] self.variantColors = ['blue', 'red', 'purple', 'green', 'orange', 'yellow', 'grey'] self.coloredVariants = False self.variantMode = False - self.LILYEXEC = None - self.tempName = None - self.inWord = None + self.tempName: pathlib.Path|None = None + self.inWord = False - def findLilyExec(self): + def findLilyExec(self) -> str: lpEnvironment = environLocal['lilypondPath'] if lpEnvironment is not None and lpEnvironment.exists(): LILYEXEC = str(lpEnvironment) # pragma: no cover @@ -196,17 +205,17 @@ def findLilyExec(self): self.LILYEXEC = LILYEXEC return LILYEXEC - def setupTools(self): + def setupTools(self) -> None: LILYEXEC = self.findLilyExec() command = [LILYEXEC, '--version'] platform = common.getPlatform() - creation_flags = subprocess.CREATE_NO_WINDOW if platform == 'win' else 0 + # CREATE_NO_WINDOW only exists on Windows + creation_flags = getattr(subprocess, 'CREATE_NO_WINDOW', 0) if platform == 'win' else 0 try: with subprocess.Popen(command, stdout=subprocess.PIPE, creationflags=creation_flags) as proc: - stdout, unused = proc.communicate() - stdout = stdout.decode(encoding='utf-8') - versionString = stdout.split()[2] + stdoutBytes, unused = proc.communicate() + versionString = stdoutBytes.decode(encoding='utf-8').split()[2] versionPieces = versionString.split('.') except OSError as exc: # pragma: no cover raise LilyTranslateException( @@ -237,11 +246,11 @@ def setupTools(self): # I had a note that said 2.12 and > should use # 'self.backendString = '--formats=' ' but doesn't seem true - def newContext(self, newContext): + def newContext(self, newContext: lyo.LyObject) -> None: self.storedContexts.append(self.context) self.context = newContext - def restoreContext(self): + def restoreContext(self) -> None: try: self.context = self.storedContexts.pop() except IndexError: # pragma: no cover @@ -249,7 +258,7 @@ def restoreContext(self): # ----------- Set a complete LilyPond Tree from a music21 object ----------# - def textFromMusic21Object(self, m21ObjectIn): + def textFromMusic21Object(self, m21ObjectIn: base.Music21Object) -> str: r''' get a proper lilypond text file for writing from a music21 object @@ -277,7 +286,7 @@ def textFromMusic21Object(self, m21ObjectIn): s = re.sub(r'\s*\n\s*\n', '\n', s).strip() return s - def loadFromMusic21Object(self, m21ObjectIn): + def loadFromMusic21Object(self, m21ObjectIn: base.Music21Object) -> None: r''' Create a LilyPond object hierarchy in self.topLevelObject from an arbitrary music21 object. @@ -285,35 +294,29 @@ def loadFromMusic21Object(self, m21ObjectIn): TODO: make lilypond automatically run makeNotation.makeTupletBrackets(s) TODO: Add tests. ''' - c = m21ObjectIn.classes - if 'Stream' in c: - if m21ObjectIn[variant.Variant]: - # has variants. so we need to make a deepcopy - m21ObjectIn = variant.makeAllVariantsReplacements(m21ObjectIn, recurse=True) - variant.makeVariantBlocks(m21ObjectIn) - - if ('Stream' not in c) or ('Measure' in c) or ('Voice' in c): + if isinstance(m21ObjectIn, stream.Stream) and m21ObjectIn[variant.Variant]: + # has variants. so we need to make a deepcopy + m21ObjectIn = variant.makeAllVariantsReplacements(m21ObjectIn, recurse=True) + variant.makeVariantBlocks(m21ObjectIn) + + if isinstance(m21ObjectIn, stream.Score): + self.loadObjectFromScore(m21ObjectIn, makeNotation=False) + elif isinstance(m21ObjectIn, stream.Opus): + self.loadObjectFromOpus(m21ObjectIn, makeNotation=False) + elif (not isinstance(m21ObjectIn, stream.Stream) + or isinstance(m21ObjectIn, (stream.Measure, stream.Voice))): scoreObj = stream.Score() partObj = stream.Part() # no need for measures or voices partObj.insert(0, m21ObjectIn) scoreObj.insert(0, partObj) self.loadObjectFromScore(scoreObj, makeNotation=False) - elif 'Part' in c: - scoreObj = stream.Score() - scoreObj.insert(0, m21ObjectIn) - self.loadObjectFromScore(scoreObj, makeNotation=False) - elif 'Score' in c: - self.loadObjectFromScore(m21ObjectIn, makeNotation=False) - elif 'Opus' in c: - self.loadObjectFromOpus(m21ObjectIn, makeNotation=False) - else: # treat as part + else: # a Part, or treated as one scoreObj = stream.Score() scoreObj.insert(0, m21ObjectIn) self.loadObjectFromScore(scoreObj, makeNotation=False) - # raise LilyTranslateException(f'Unknown stream type {m21ObjectIn.__class__}') - def loadObjectFromOpus(self, opusIn=None, makeNotation=True): + def loadObjectFromOpus(self, opusIn: stream.Opus, makeNotation: bool = True) -> None: r''' creates a filled topLevelObject (lily.lilyObjects.LyLilypondTop) whose string representation accurately reflects all the Score objects @@ -324,7 +327,7 @@ def loadObjectFromOpus(self, opusIn=None, makeNotation=True): >>> #_DOCS_SHOW lpc.loadObjectFromOpus(fifeOpus, makeNotation=False) >>> #_DOCS_SHOW lpc.showPDF() ''' - contents = [] + contents: list[lyo.LyObject|str] = [] lpVersionScheme = self.versionScheme lpHeaderScheme = self.headerScheme lpColorScheme = lyo.LyEmbeddedScm(self.colorDef) @@ -353,9 +356,9 @@ def loadObjectFromOpus(self, opusIn=None, makeNotation=True): contents.append(lpLayout) - self.context.contents = contents + self.setContextContents(contents) - def loadObjectFromScore(self, scoreIn=None, makeNotation=True): + def loadObjectFromScore(self, scoreIn: stream.Score, makeNotation: bool = True) -> None: r''' creates a filled topLevelObject (lily.lilyObjects.LyLilypondTop) @@ -381,16 +384,16 @@ def loadObjectFromScore(self, scoreIn=None, makeNotation=True): lpOutputDefBody = lyo.LyOutputDefBody(outputDefHead=lpOutputDefHead) lpOutputDef = lyo.LyOutputDef(outputDefBody=lpOutputDefBody) lpLayout = lyo.LyLayout() - contents = [lpVersionScheme, lpHeaderScheme, lpColorScheme, - lpHeader, lpScoreBlock, lpOutputDef, lpLayout] + contents: list[lyo.LyObject|str] = [lpVersionScheme, lpHeaderScheme, lpColorScheme, + lpHeader, lpScoreBlock, lpOutputDef, lpLayout] if scoreIn.metadata is not None: self.setHeaderFromMetadata(scoreIn.metadata, lpHeader=lpHeader) - self.context.contents = contents + self.setContextContents(contents) # ------ return Lily objects or append to the current context -----------# - def lyScoreBlockFromScore(self, scoreIn): + def lyScoreBlockFromScore(self, scoreIn: stream.Score) -> lyo.LyScoreBlock: lpCompositeMusic = lyo.LyCompositeMusic() self.newContext(lpCompositeMusic) @@ -418,7 +421,7 @@ def lyScoreBlockFromScore(self, scoreIn): return lpScoreBlock - def lyPartsAndOssiaInitFromScore(self, scoreIn): + def lyPartsAndOssiaInitFromScore(self, scoreIn: stream.Score) -> lyo.LyMusicList: r''' Takes in a score and returns a block that starts each part context and variant context with an identifier and {\stopStaff s1*n} (or s, whatever is needed for the duration) @@ -512,7 +515,7 @@ def lyPartsAndOssiaInitFromScore(self, scoreIn): ''' lpMusicList = lyo.LyMusicList() - musicList = [] + musicList: list[lyo.LyObject|str] = [] lpMusic = r'{ \stopStaff %s}' for p in scoreIn.parts: @@ -525,7 +528,7 @@ def lyPartsAndOssiaInitFromScore(self, scoreIn): music=lpMusic % spacerDuration) musicList.append(lpPrefixCompositeMusicPart) - variantsAddedForPart = [] + variantsAddedForPart: list[str] = [] for v in p.getElementsByClass(variant.Variant): variantName = v.groups[0] if variantName not in variantsAddedForPart: @@ -558,7 +561,7 @@ def lyPartsAndOssiaInitFromScore(self, scoreIn): return lpMusicList - def getLySpacersFromStream(self, streamIn, measuresOnly=True): + def getLySpacersFromStream(self, streamIn: stream.Stream, measuresOnly: bool = True) -> str: # noinspection PyShadowingNames r''' Creates a series of Spacer objects for the measures in a Stream Part. @@ -606,7 +609,11 @@ def getLySpacersFromStream(self, streamIn, measuresOnly=True): return returnString - def lyGroupedMusicListFromScoreWithParts(self, scoreIn, scoreInit=None): + def lyGroupedMusicListFromScoreWithParts( + self, + scoreIn: stream.Score, + scoreInit: lyo.LyMusicList|None = None, + ) -> lyo.LyGroupedMusicList: # noinspection PyShadowingNames,GrazieInspection r''' More complex example showing how the score can be set up with ossia parts: @@ -673,7 +680,7 @@ def lyGroupedMusicListFromScoreWithParts(self, scoreIn, scoreInit=None): ''' - compositeMusicList = [] + compositeMusicList: list[lyo.LyObject|str] = [] lpGroupedMusicList = lyo.LyGroupedMusicList() lpSimultaneousMusic = lyo.LySimultaneousMusic() @@ -699,7 +706,12 @@ def lyGroupedMusicListFromScoreWithParts(self, scoreIn, scoreInit=None): return lpGroupedMusicList - def lyNewLyricsFromStream(self, streamIn, streamId=None, alignment='alignBelowContext'): + def lyNewLyricsFromStream( + self, + streamIn: stream.Stream, + streamId: str|None = None, + alignment: str = 'alignBelowContext', + ) -> lyo.LyNewLyrics: r''' returns a LyNewLyrics object @@ -726,9 +738,9 @@ def lyNewLyricsFromStream(self, streamIn, streamId=None, alignment='alignBelowCo streamId = '#' + lyo.LyObject().quoteString(streamId) - lpGroupedMusicLists = [] + lpGroupedMusicLists: list[lyo.LyGroupedMusicList|str] = [] for lyricNum in sorted(lyricsDict): - lyricList = [] + lyricList: list[lyo.LyObject|str] = [] lpAlignmentProperty = lyo.LyPropertyOperation(mode='set', value1=alignment, value2=streamId) @@ -736,6 +748,8 @@ def lyNewLyricsFromStream(self, streamIn, streamId=None, alignment='alignBelowCo self.inWord = False for el in lyricsDict[lyricNum]: + if isinstance(el, list): # only arises from .lyrics(recurse=True) + continue lpLyricElement = self.lyLyricElementFromM21Lyric(el) lyricList.append(lpLyricElement) @@ -751,7 +765,7 @@ def lyNewLyricsFromStream(self, streamIn, streamId=None, alignment='alignBelowCo return lpNewLyrics - def lyLyricElementFromM21Lyric(self, m21Lyric): + def lyLyricElementFromM21Lyric(self, m21Lyric: note.Lyric|None) -> lyo.LyLyricElement: r''' Returns a :class:`~music21.lily.lilyObjects.LyLyricElement` object from a :class:`~music21.note.Lyric` object. @@ -778,15 +792,10 @@ def lyLyricElementFromM21Lyric(self, m21Lyric): False ''' - if hasattr(self, 'inWord'): - inWord = self.inWord - else: - inWord = False + inWord = self.inWord el = m21Lyric - if el is None and inWord: - text = ' _ ' - elif el is None and inWord is False: + if el is None: text = ' _ ' elif el.text == '': text = ' _ ' @@ -807,7 +816,11 @@ def lyLyricElementFromM21Lyric(self, m21Lyric): lpLyricElement = lyo.LyLyricElement(text) return lpLyricElement - def lySequentialMusicFromStream(self, streamIn, beforeMatter=None): + def lySequentialMusicFromStream( + self, + streamIn: stream.Stream, + beforeMatter: str|None = None, + ) -> lyo.LySequentialMusic: r''' returns a LySequentialMusic object from a stream @@ -828,7 +841,7 @@ def lySequentialMusicFromStream(self, streamIn, beforeMatter=None): } ''' - musicList = [] + musicList: list[lyo.LyObject|str] = [] lpMusicList = lyo.LyMusicList(contents=musicList) lpSequentialMusic = lyo.LySequentialMusic(musicList=lpMusicList, @@ -846,11 +859,11 @@ def lySequentialMusicFromStream(self, streamIn, beforeMatter=None): # pylint: disable=redefined-builtin def lyPrefixCompositeMusicFromStream( self, - streamIn, - contextType=None, - type=None, - beforeMatter=None - ): + streamIn: stream.Stream, + contextType: str|None = None, + type: str|None = None, + beforeMatter: str|None = None, + ) -> lyo.LyPrefixCompositeMusic: # noinspection PyShadowingNames r''' returns an LyPrefixCompositeMusic object from @@ -882,7 +895,7 @@ def lyPrefixCompositeMusicFromStream( compositeMusicType = type optionalId = None - contextModList = [] + contextModList: list[str] = [] c = streamIn.classes if contextType is None: @@ -916,10 +929,7 @@ def lyPrefixCompositeMusicFromStream( if compositeMusicType is None: compositeMusicType = 'new' - if contextModList: - contextMod = lyo.LyContextModification(contextModList) - else: - contextMod = None + contextMod = lyo.LyContextModification(contextModList) if contextModList else None lpPrefixCompositeMusic = lyo.LyPrefixCompositeMusic(type=compositeMusicType, optionalId=optionalId, @@ -928,7 +938,7 @@ def lyPrefixCompositeMusicFromStream( music=lpMusic) return lpPrefixCompositeMusic - def appendObjectsToContextFromStream(self, streamObject): + def appendObjectsToContextFromStream(self, streamObject: stream.Stream) -> None: r''' takes a Stream and appends all the elements in it to the current context's .contents list, and deals with creating Voices in it. It also deals with @@ -975,18 +985,18 @@ def appendObjectsToContextFromStream(self, streamObject): >> ''' from music21.stream.iterator import OffsetIterator - for groupedElements in OffsetIterator(streamObject): + for groupedElements in OffsetIterator[base.Music21Object](streamObject): # print(groupedElements) if len(groupedElements) == 1: # one thing at that moment - el = groupedElements[0] - el.activeSite = streamObject - self.appendM21ObjectToContext(el) + soleElement = groupedElements[0] + soleElement.activeSite = streamObject + self.appendM21ObjectToContext(soleElement) else: # voices or other More than one thing at once # if voices - voiceList = [] - variantList = [] - otherList = [] + voiceList: list[stream.Voice] = [] + variantList: list[variant.Variant] = [] + otherList: list[base.Music21Object] = [] for el in groupedElements: if isinstance(el, stream.Voice): voiceList.append(el) @@ -1004,7 +1014,7 @@ def appendObjectsToContextFromStream(self, streamObject): coloredVariants=self.coloredVariants) if voiceList: - musicList2 = [] + musicList2: list[lyo.LyObject|str] = [] lp2GroupedMusicList = lyo.LyGroupedMusicList() lp2SimultaneousMusic = lyo.LySimultaneousMusic() lp2MusicList = lyo.LyMusicList() @@ -1018,8 +1028,7 @@ def appendObjectsToContextFromStream(self, streamObject): lp2MusicList.contents = musicList2 - contextObject = self.context - currentMusicList = contextObject.contents + currentMusicList = self.contextContents() currentMusicList.append(lp2GroupedMusicList) lp2GroupedMusicList.setParent(self.context) @@ -1027,7 +1036,7 @@ def appendObjectsToContextFromStream(self, streamObject): for el in otherList: self.appendM21ObjectToContext(el) - def appendM21ObjectToContext(self, thisObject): + def appendM21ObjectToContext(self, thisObject: base.Music21Object) -> None: r''' converts any type of object into a lilyObject of LyMusic ( LySimpleMusic, LyEmbeddedScm etc.) type @@ -1036,28 +1045,22 @@ def appendM21ObjectToContext(self, thisObject): return # treat complex duration objects as multiple objects - c = thisObject.classes - - if 'Stream' not in c and thisObject.duration.type == 'complex': + if not isinstance(thisObject, stream.Stream) and thisObject.duration.type == 'complex': thisObjectSplit = thisObject.splitAtDurations() for subComponent in thisObjectSplit: self.appendM21ObjectToContext(subComponent) return contextObject = self.context - if hasattr(contextObject, 'contents'): - currentMusicList = contextObject.contents - else: # pragma: no cover - raise LilyTranslateException( - f'Cannot get a currentMusicList from contextObject {contextObject!r}') + currentMusicList = self.contextContents() - if hasattr(thisObject, 'startTransparency') and thisObject.startTransparency is True: + if getattr(thisObject, 'startTransparency', False) is True: # old hack, replace with the better "hidden" attribute lyScheme = lyo.LyEmbeddedScm(self.transparencyStartScheme) currentMusicList.append(lyScheme) - lyObject = None - if 'Measure' in c: + lyObject: lyo.LyObject|None = None + if isinstance(thisObject, stream.Measure): # lilypond does not put groups around measures # it does however need barline ends # also, if variantMode is True, the last note in each "measure" should have \noBeam @@ -1075,46 +1078,43 @@ def appendM21ObjectToContext(self, thisObject): self.appendObjectsToContextFromStream(thisObject) self.currentMeasure = thisObject - elif 'Stream' in c: - # try: + elif isinstance(thisObject, stream.Stream): lyObject = self.lyPrefixCompositeMusicFromStream(thisObject) currentMusicList.append(lyObject) lyObject.setParent(contextObject) - elif 'Note' in c or 'Rest' in c: + elif isinstance(thisObject, (note.Note, note.Rest)): self.appendContextFromNoteOrRest(thisObject) - elif 'Chord' in c: + elif isinstance(thisObject, chord.Chord): self.appendContextFromChord(thisObject) - elif 'Clef' in c: + elif isinstance(thisObject, clef.Clef): lyObject = self.lyEmbeddedScmFromClef(thisObject) currentMusicList.append(lyObject) lyObject.setParent(contextObject) - elif 'KeySignature' in c: + elif isinstance(thisObject, key.KeySignature): lyObject = self.lyEmbeddedScmFromKeySignature(thisObject) currentMusicList.append(lyObject) lyObject.setParent(contextObject) - elif 'TimeSignature' in c and self.variantMode is False: + elif isinstance(thisObject, meter.TimeSignature) and self.variantMode is False: lyObject = self.lyEmbeddedScmFromTimeSignature(thisObject) currentMusicList.append(lyObject) lyObject.setParent(contextObject) - elif 'Variant' in c: + elif isinstance(thisObject, variant.Variant): self.appendContextFromVariant(thisObject, coloredVariants=self.coloredVariants) - elif 'SystemLayout' in c: + elif isinstance(thisObject, layout.SystemLayout): lyObject = lyo.LyEmbeddedScm(r'\break') currentMusicList.append(lyObject) lyObject.setParent(contextObject) - elif 'PageLayout' in c: + elif isinstance(thisObject, layout.PageLayout): lyObject = lyo.LyEmbeddedScm(r'\pageBreak') currentMusicList.append(lyObject) lyObject.setParent(contextObject) - else: - lyObject = None - if hasattr(thisObject, 'stopTransparency') and thisObject.stopTransparency is True: + if getattr(thisObject, 'stopTransparency', False) is True: # old hack, replace with the better "hidden" attribute lyScheme = lyo.LyEmbeddedScm(self.transparencyStopScheme) currentMusicList.append(lyScheme) - def appendContextFromNoteOrRest(self, noteOrRest): + def appendContextFromNoteOrRest(self, noteOrRest: note.GeneralNote) -> None: r''' appends lySimpleMusicFromNoteOrRest to the current context. @@ -1175,11 +1175,11 @@ def appendContextFromNoteOrRest(self, noteOrRest): self.appendStemCode(noteOrRest) lpSimpleMusic = self.lySimpleMusicFromNoteOrRest(noteOrRest) - self.context.contents.append(lpSimpleMusic) + self.contextContents().append(lpSimpleMusic) lpSimpleMusic.setParent(self.context) self.setContextForTupletStop(noteOrRest) - def appendContextFromChord(self, chord): + def appendContextFromChord(self, chordIn: chord.Chord) -> None: r''' appends lySimpleMusicFromChord to the current context. @@ -1220,16 +1220,34 @@ def appendContextFromChord(self, chord): ''' - self.setContextForTupletStart(chord) - self.appendBeamCode(chord) - self.appendStemCode(chord) + self.setContextForTupletStart(chordIn) + self.appendBeamCode(chordIn) + self.appendStemCode(chordIn) - lpSimpleMusic = self.lySimpleMusicFromChord(chord) - self.context.contents.append(lpSimpleMusic) + lpSimpleMusic = self.lySimpleMusicFromChord(chordIn) + self.contextContents().append(lpSimpleMusic) lpSimpleMusic.setParent(self.context) - self.setContextForTupletStop(chord) + self.setContextForTupletStop(chordIn) - def lySimpleMusicFromNoteOrRest(self, noteOrRest): + def contextContents(self) -> list[lyo.LyObject|str]: + ''' + The `.contents` list of the current context; raises a + LilyTranslateException if the context cannot hold contents. + ''' + contents: t.Any = getattr(self.context, 'contents', None) + if contents is None: # pragma: no cover + raise LilyTranslateException( + f'Cannot get a currentMusicList from contextObject {self.context!r}') + return contents + + def setContextContents(self, contents: list[lyo.LyObject|str]) -> None: + ''' + Replace the `.contents` list of the current context, reparenting its members. + ''' + context: t.Any = self.context + context.contents = contents + + def lySimpleMusicFromNoteOrRest(self, noteOrRest: note.GeneralNote) -> lyo.LySimpleMusic: r''' returns a lilyObjects.LySimpleMusic object for the generalNote containing this hierarchy:: @@ -1267,9 +1285,7 @@ def lySimpleMusicFromNoteOrRest(self, noteOrRest): >>> print(sm) s 4 ''' - c = noteOrRest.classes - - simpleElementParts = [] + simpleElementParts: list[lyo.LyObject|str] = [] # https://lilypond.org/doc/v2.22/Documentation/notation/inside-the-staff#coloring-objects if noteOrRest.hasStyleInformation: @@ -1280,18 +1296,18 @@ def lySimpleMusicFromNoteOrRest(self, noteOrRest): simpleElementParts.append(noteheadColor) simpleElementParts.append(stemColor) - if 'Note' in c: + if isinstance(noteOrRest, note.Note): if not noteOrRest.hasStyleInformation or noteOrRest.style.hideObjectOnPrint is False: - lpPitch = self.lyPitchFromPitch(noteOrRest.pitch) - simpleElementParts.append(lpPitch) - if noteOrRest.pitch.accidental is not None: - if noteOrRest.pitch.accidental.displayType == 'always': + notePitch = noteOrRest.pitch + simpleElementParts.append(self.lyPitchFromPitch(notePitch)) + if notePitch.accidental is not None: + if notePitch.accidental.displayType == 'always': simpleElementParts.append('! ') - if noteOrRest.pitch.accidental.displayStyle == 'parentheses': + if notePitch.accidental.displayStyle == 'parentheses': simpleElementParts.append('? ') else: simpleElementParts.append('s ') - elif 'Rest' in c: + elif isinstance(noteOrRest, note.Rest): if noteOrRest.hasStyleInformation and noteOrRest.style.hideObjectOnPrint: simpleElementParts.append('s ') else: @@ -1315,7 +1331,7 @@ def lySimpleMusicFromNoteOrRest(self, noteOrRest): return mlSM - def appendBeamCode(self, noteOrChord): + def appendBeamCode(self, noteOrChord: note.GeneralNote) -> None: r''' Adds an LyEmbeddedScm object to the context's contents if the object's has a .beams attribute. @@ -1368,16 +1384,16 @@ def appendBeamCode(self, noteOrChord): if leftBeams > 0: beamText = rf'''\set stemLeftBeamCount = #{leftBeams}''' lpBeamScheme = lyo.LyEmbeddedScm(beamText) - self.context.contents.append(lpBeamScheme) + self.contextContents().append(lpBeamScheme) lpBeamScheme.setParent(self.context) if rightBeams > 0: beamText = fr'''\set stemRightBeamCount = #{rightBeams}''' lpBeamScheme = lyo.LyEmbeddedScm(beamText) - self.context.contents.append(lpBeamScheme) + self.contextContents().append(lpBeamScheme) lpBeamScheme.setParent(self.context) - def appendStemCode(self, noteOrChord): + def appendStemCode(self, noteOrChord: note.GeneralNote) -> None: r''' Adds an LyEmbeddedScm object to the context's contents if the object's stem direction is set (currently, only "up" and "down" are supported). @@ -1400,10 +1416,10 @@ def appendStemCode(self, noteOrChord): if stemDirection in ['UP', 'DOWN']: stemFile = fr'''\once \override Stem.direction = #{stemDirection} ''' lpStemScheme = lyo.LyEmbeddedScm(stemFile) - self.context.contents.append(lpStemScheme) + self.contextContents().append(lpStemScheme) lpStemScheme.setParent(self.context) - def lySimpleMusicFromChord(self, chordObj): + def lySimpleMusicFromChord(self, chordObj: chord.Chord) -> lyo.LySimpleMusic: ''' >>> conv = lily.translate.LilypondConverter() @@ -1419,6 +1435,7 @@ def lySimpleMusicFromChord(self, chordObj): >>> print(conv.lySimpleMusicFromChord(c1)) s 2.. ''' + lpChordBody: lyo.LyChordBody|lyo.LyPitch self.appendBeamCode(chordObj) if not chordObj.hasStyleInformation or chordObj.style.hideObjectOnPrint is not True: @@ -1426,7 +1443,7 @@ def lySimpleMusicFromChord(self, chordObj): chordBodyElements = [] for p in chordObj.pitches: - chordBodyElementParts = [] + chordBodyElementParts: list[lyo.LyObject|str] = [] lpPitch = self.lyPitchFromPitch(p) chordBodyElementParts.append(lpPitch) if p.accidental is not None: @@ -1452,12 +1469,12 @@ def lySimpleMusicFromChord(self, chordObj): return mlSM # TODO: Chord beaming - def postEventsFromObject(self, generalNote): + def postEventsFromObject(self, generalNote: note.GeneralNote) -> list[lyo.LyObject|str]: r''' attaches events that apply to notes and chords (and some other things) equally ''' - postEvents = [] + postEvents: list[lyo.LyObject|str] = [] # remove this hack once lyrics work # if generalNote.lyric is not None: # hack that uses markup @@ -1474,35 +1491,35 @@ def postEventsFromObject(self, generalNote): postEvents.append(r'\fermata ') return postEvents - def lyPitchFromPitch(self, pitch): + def lyPitchFromPitch(self, pitchObj: pitch.Pitch) -> lyo.LyPitch: r''' converts a music21.pitch.Pitch object to a lily.lilyObjects.LyPitch object. ''' - baseName = self.baseNameFromPitch(pitch) - octaveModChars = self.octaveCharactersFromPitch(pitch) + baseName = self.baseNameFromPitch(pitchObj) + octaveModChars = self.octaveCharactersFromPitch(pitchObj) lyPitch = lyo.LyPitch(baseName, octaveModChars) return lyPitch - def baseNameFromPitch(self, pitch): + def baseNameFromPitch(self, pitchObj: pitch.Pitch) -> str: r''' returns a string of the base name (including accidental) for a music21 pitch ''' - baseName = pitch.step.lower() - if pitch.accidental is not None: - if pitch.accidental.name in self.accidentalConvert: - baseName += self.accidentalConvert[pitch.accidental.name] + baseName = pitchObj.step.lower() + if pitchObj.accidental is not None: + if pitchObj.accidental.name in self.accidentalConvert: + baseName += self.accidentalConvert[pitchObj.accidental.name] return baseName - def octaveCharactersFromPitch(self, pitch): + def octaveCharactersFromPitch(self, pitchObj: pitch.Pitch) -> str: r''' returns a string of single-quotes or commas or '' representing the octave of a :class:`~music21.pitch.Pitch` object ''' - implicitOctave = pitch.implicitOctave + implicitOctave = pitchObj.implicitOctave if implicitOctave < 3: correctedOctave = 3 - implicitOctave octaveModChars = ',' * correctedOctave # C2 = c, C1 = c,, @@ -1514,7 +1531,7 @@ def octaveCharactersFromPitch(self, pitch): def lyMultipliedDurationFromDuration( self, durationObj: duration.Duration|duration.DurationTuple, - ): + ) -> lyo.LyMultipliedDuration: r''' take a simple Duration (that is, one with one DurationTuple) object and return a LyMultipliedDuration object: @@ -1581,7 +1598,7 @@ def lyMultipliedDurationFromDuration( f'DurationException: Cannot translate durationObject {durationObj}: {de}') return multipliedDuration - def lyEmbeddedScmFromClef(self, clefObj): + def lyEmbeddedScmFromClef(self, clefObj: clef.Clef) -> lyo.LyEmbeddedScm: # noinspection PyShadowingNames r''' converts a Clef object to a @@ -1627,7 +1644,7 @@ def lyEmbeddedScmFromClef(self, clefObj): lpEmbeddedScm.content = clefScheme return lpEmbeddedScm - def lyEmbeddedScmFromKeySignature(self, keyObj): + def lyEmbeddedScmFromKeySignature(self, keyObj: key.KeySignature) -> lyo.LyEmbeddedScm: # noinspection PyShadowingNames r''' converts a Key or KeySignature object @@ -1646,11 +1663,10 @@ def lyEmbeddedScmFromKeySignature(self, keyObj): \key fis \major ''' - if not isinstance(keyObj, key.Key): - keyObj = keyObj.asKey('major') + keyAsKey = keyObj if isinstance(keyObj, key.Key) else keyObj.asKey('major') - p = keyObj.tonic - m = keyObj.mode + p = keyAsKey.tonic + m = keyAsKey.mode pn = self.baseNameFromPitch(p) @@ -1664,7 +1680,7 @@ def lyEmbeddedScmFromKeySignature(self, keyObj): lpEmbeddedScm.content = keyScheme return lpEmbeddedScm - def lyEmbeddedScmFromTimeSignature(self, ts): + def lyEmbeddedScmFromTimeSignature(self, ts: meter.TimeSignature) -> lyo.LyEmbeddedScm: # noinspection PyShadowingNames r''' convert a :class:`~music21.meter.TimeSignature` object @@ -1680,7 +1696,7 @@ def lyEmbeddedScmFromTimeSignature(self, ts): lpEmbeddedScm.content = keyScheme return lpEmbeddedScm - def setContextForTupletStart(self, inObj): + def setContextForTupletStart(self, inObj: base.Music21Object) -> lyo.LyMusicList|None: r''' if the inObj has tuplets then we set a new context for the tuplets and anything up till a tuplet stop. @@ -1706,7 +1722,11 @@ def setContextForTupletStart(self, inObj): else: return None - def setContextForTimeFraction(self, numerator, denominator): + def setContextForTimeFraction( + self, + numerator: int|str, + denominator: int|str, + ) -> lyo.LyMusicList: r''' Explicitly starts a new context for scaled music (tuplets, etc.) for the given numerator and denominator (either an int or a string or unicode) @@ -1749,17 +1769,13 @@ def setContextForTimeFraction(self, numerator, denominator): lpPrefixCompositeMusic = lyo.LyPrefixCompositeMusic(type='times', fraction=fraction, music=lpSequentialMusic) - currentContents = self.context.contents - if currentContents is None: # pragma: no cover - raise LilyTranslateException( - f'Cannot find contents for self.context: {self.context!r} ') - + currentContents = self.contextContents() currentContents.append(lpPrefixCompositeMusic) lpPrefixCompositeMusic.setParent(self.context) self.newContext(lpMusicList) return lpMusicList - def setContextForTupletStop(self, inObj): + def setContextForTupletStop(self, inObj: base.Music21Object) -> None: r''' Reverse of setContextForTupletStart ''' @@ -1767,15 +1783,18 @@ def setContextForTupletStop(self, inObj): return elif inObj.duration.tuplets[0].type == 'stop': self.restoreContext() - else: - return None - def appendContextFromVariant(self, variantObjectOrList, activeSite=None, coloredVariants=False): + def appendContextFromVariant( + self, + variantObjectOrList: variant.Variant|list[variant.Variant], + activeSite: stream.Stream|None = None, + coloredVariants: bool = False, + ) -> None: r''' Create a new context from the variant object or a list of variants and append. ''' - musicList = [] - longestReplacedElements = [] + musicList: list[lyo.LyObject|str] = [] + longestReplacedElements: stream.Stream = stream.Stream() if isinstance(variantObjectOrList, variant.Variant): variantObject = variantObjectOrList @@ -1788,8 +1807,8 @@ def appendContextFromVariant(self, variantObjectOrList, activeSite=None, colored musicList.append(lpSequentialMusicStandard) elif isinstance(variantObjectOrList, list): - longestReplacementLength = -1 - variantDict = {} + longestReplacementLength = -1.0 + variantDict: dict[str, list[variant.Variant]] = {} for variantObject in variantObjectOrList: if variantObject.groups: variantName = variantObject.groups[0] @@ -1831,15 +1850,16 @@ def appendContextFromVariant(self, variantObjectOrList, activeSite=None, colored lp2GroupedMusicList = lyo.LyGroupedMusicList() lp2GroupedMusicList.simultaneousMusic = lp2SimultaneousMusic - contextObject = self.context - currentMusicList = contextObject.contents + currentMusicList = self.contextContents() currentMusicList.append(lp2GroupedMusicList) lp2GroupedMusicList.setParent(self.context) - def lyPrefixCompositeMusicFromRelatedVariants(self, - variantList, - activeSite=None, - coloredVariants=False): + def lyPrefixCompositeMusicFromRelatedVariants( + self, + variantList: list[variant.Variant], + activeSite: stream.Stream|None = None, + coloredVariants: bool = False, + ) -> tuple[lyo.LyPrefixCompositeMusic, stream.Stream]: # noinspection PyShadowingNames r''' @@ -1939,12 +1959,13 @@ def lyPrefixCompositeMusicFromRelatedVariants(self, # Order List - def findOffsetOfFirstNonSpacerElement(inputStream): + def findOffsetOfFirstNonSpacerElement(inputStream: stream.Stream) -> float: for el in inputStream: if isinstance(el, note.Rest) and el.style.hideObjectOnPrint: pass else: - return inputStream.elementOffset(el) + return float(inputStream.elementOffset(el)) + return 0.0 variantList.sort(key=lambda vv: findOffsetOfFirstNonSpacerElement(vv._stream)) @@ -1953,9 +1974,11 @@ def findOffsetOfFirstNonSpacerElement(inputStream): re0 = replacedElements[0] replacedElementsClef = re0.clef or re0.getContextByClass(clef.Clef) - variantContainerStream = variantList[0].getContextByClass(stream.Part) + variantContainerStream: stream.Stream|None = variantList[0].getContextByClass(stream.Part) if variantContainerStream is None: - variantContainerStream = variantList[0].getContextByClass('Stream') + variantContainerStream = variantList[0].getContextByClass(stream.Stream) + if variantContainerStream is None: # pragma: no cover + raise LilyTranslateException(f'Cannot find a container stream for {variantList[0]}') variantList[0].insert(0.0, replacedElementsClef) variantName = variantList[0].groups[0] @@ -1975,9 +1998,9 @@ def findOffsetOfFirstNonSpacerElement(inputStream): ####################### - musicList = [] + musicList: list[lyo.LyObject|str] = [] highestOffsetSoFar = 0.0 - longestVariant = None + longestVariant = variantList[-1] self.variantMode = True @@ -2018,6 +2041,7 @@ def findOffsetOfFirstNonSpacerElement(inputStream): replacedElementsLength = vStripped.replacementQuarterLength variantLength = vStripped.containedHighestTime - firstOffset + lpOssiaMusicVariant: lyo.LyObject if variantLength != replacedElementsLength: numerator, denominator = common.decimalToTuplet( replacedElementsLength / variantLength) @@ -2055,10 +2079,12 @@ def findOffsetOfFirstNonSpacerElement(inputStream): return lpPrefixCompositeMusicVariant, replacedElements - def lyPrefixCompositeMusicFromVariant(self, - variantObject, - replacedElements, - coloredVariants=False): + def lyPrefixCompositeMusicFromVariant( + self, + variantObject: variant.Variant, + replacedElements: stream.Stream, + coloredVariants: bool = False, + ) -> lyo.LyPrefixCompositeMusic: # noinspection PyShadowingNames r''' @@ -2100,9 +2126,11 @@ def lyPrefixCompositeMusicFromVariant(self, ''' replacedElementsClef = replacedElements[0].getContextByClass(clef.Clef) - variantContainerStream = variantObject.getContextByClass(stream.Part) + variantContainerStream: stream.Stream|None = variantObject.getContextByClass(stream.Part) if variantContainerStream is None: - variantContainerStream = variantObject.getContextByClass('Stream') + variantContainerStream = variantObject.getContextByClass(stream.Stream) + if variantContainerStream is None: # pragma: no cover + raise LilyTranslateException(f'Cannot find a container stream for {variantObject}') if replacedElementsClef is not None: if replacedElementsClef not in variantObject.elements: @@ -2126,7 +2154,7 @@ def lyPrefixCompositeMusicFromVariant(self, for n in variantObject._stream.recurse().notesAndRests: n.style.color = color - musicList = [] + musicList: list[lyo.LyObject|str] = [] varFilter = [r for r in variantObject.getElementsByClass(note.Rest) if r.style.hideObjectOnPrint] @@ -2210,7 +2238,7 @@ def lyPrefixCompositeMusicFromVariant(self, # currentMusicList.append(lp2GroupedMusicList) # lp2GroupedMusicList.setParent(self.context) - def lyOssiaMusicFromVariant(self, variantIn): + def lyOssiaMusicFromVariant(self, variantIn: variant.Variant) -> lyo.LyOssiaMusic: r''' returns a LyOssiaMusic object from a stream @@ -2232,7 +2260,7 @@ def lyOssiaMusicFromVariant(self, variantIn): } ''' - musicList = [] + musicList: list[lyo.LyObject|str] = [] lpMusicList = lyo.LyMusicList(contents=musicList) lpOssiaMusic = lyo.LyOssiaMusic(musicList=lpMusicList) @@ -2251,7 +2279,11 @@ def lyOssiaMusicFromVariant(self, variantIn): return lpOssiaMusic - def setHeaderFromMetadata(self, metadataObject=None, lpHeader=None): + def setHeaderFromMetadata( + self, + metadataObject: metadata.Metadata|None = None, + lpHeader: lyo.LyLilypondHeader|None = None, + ) -> lyo.LyLilypondHeader: # noinspection PyShadowingNames r''' Returns a lilypond.lilyObjects.LyLilypondHeader object @@ -2302,7 +2334,7 @@ def setHeaderFromMetadata(self, metadataObject=None, lpHeader=None): lpHeaderBody.assignments = lpHeaderBodyAssignments return lpHeader - def closeMeasure(self, barChecksOnly=False): + def closeMeasure(self, barChecksOnly: bool = False) -> lyo.LyEmbeddedScm|None: # noinspection PyShadowingNames r''' return a LyObject or None for the end of the previous Measure @@ -2348,7 +2380,7 @@ def closeMeasure(self, barChecksOnly=False): lpBarline.content = barString return lpBarline - def getSchemeForPadding(self, measureObject): + def getSchemeForPadding(self, measureObject: stream.Measure) -> lyo.LyEmbeddedScm|None: r''' lilypond partial durations are very strange and are really of type LyMultipliedDuration. You notate how many @@ -2386,7 +2418,7 @@ def getSchemeForPadding(self, measureObject): return lyObject # -------------display and converter routines ---------------------# - def writeLyFile(self, ext='', fp=None): + def writeLyFile(self, ext: str = '', fp: str|pathlib.Path|None = None) -> pathlib.Path: r''' writes the contents of the self.topLevelObject to a file. @@ -2406,8 +2438,13 @@ def writeLyFile(self, ext='', fp=None): return self.tempName # noinspection PyShadowingBuiltins - def runThroughLily(self, format=None, - backend=None, fileName=None, skipWriting=False): + def runThroughLily( + self, + format: str, + backend: str|None = None, + fileName: str|pathlib.Path|None = None, + skipWriting: bool = False, + ) -> pathlib.Path: r''' creates a .ly file from self.topLevelObject via .writeLyFile then runs the file through LilyPond. @@ -2449,7 +2486,7 @@ def runThroughLily(self, format=None, fileForm = fileEnd return pathlib.Path(fileForm) - def createPDF(self, fileName=None): + def createPDF(self, fileName: str|pathlib.Path|None = None) -> pathlib.Path: r''' create a PDF file from self.topLevelObject and return the filepath of the file. @@ -2459,7 +2496,7 @@ def createPDF(self, fileName=None): lilyFile = self.runThroughLily(backend='ps', format='pdf', fileName=fileName) return lilyFile - def showPDF(self): + def showPDF(self) -> None: r''' create an SVG file from self.topLevelObject, show it with your pdf reader (often Adobe Acrobat/Adobe Reader or Apple Preview) @@ -2479,7 +2516,7 @@ def showPDF(self): command = '' os.system(command) - def createPNG(self, fileName=None): + def createPNG(self, fileName: str|pathlib.Path|None = None) -> pathlib.Path: r''' create a PNG file from self.topLevelObject and return the filepath of the file. @@ -2500,7 +2537,7 @@ def createPNG(self, fileName=None): pass # no big deal probably return lilyFile - def showPNG(self): + def showPNG(self) -> t.Any: r''' Take the object, run it through LilyPond, and then show it as a PNG file. On Windows, the PNG file will not be deleted, so you will need to clean out @@ -2515,7 +2552,7 @@ def showPNG(self): # self.showImageDirect(lilyFile) return SubConverter().launch(lilyFile, fmt='png') - def createSVG(self, fileName=None): + def createSVG(self, fileName: str|pathlib.Path|None = None) -> pathlib.Path: r''' create an SVG file from self.topLevelObject and return the filepath of the file. @@ -2525,7 +2562,7 @@ def createSVG(self, fileName=None): lilyFile = self.runThroughLily(format='svg', backend='svg', fileName=fileName) return lilyFile - def showSVG(self, fileName=None): + def showSVG(self, fileName: str|pathlib.Path|None = None) -> t.Any: r''' create an SVG file from self.topLevelObject, show it with your svg reader (often Internet Explorer on PC) @@ -2551,7 +2588,6 @@ def testExplicitConvertChorale(self): # print(lpc.topLevelObject) def testComplexDuration(self): - from music21 import meter s = stream.Stream() n1 = note.Note('C') # test no octave also! n1.duration.quarterLength = 2.5 # BUG 2.3333333333 doesn't work right @@ -2617,7 +2653,6 @@ def xtestSlowConvertOpus(self): fifeOpus.show('lily.png') def xtestBreve(self): - from music21 import meter n = note.Note('C5') n.duration.quarterLength = 8.0 m = stream.Measure() From 61a51ff089a0da3113f65095e352354052056df1 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 6 Aug 2026 16:30:37 -1000 Subject: [PATCH 2/4] Trim the module docstring; note where the grammar lives now AI-assisted (Claude) --- music21/lily/lilyObjects.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/music21/lily/lilyObjects.py b/music21/lily/lilyObjects.py index 0a1af9460..62d42e53c 100644 --- a/music21/lily/lilyObjects.py +++ b/music21/lily/lilyObjects.py @@ -13,22 +13,12 @@ local computer, can automatically generate .pdf, .png, and .svg versions of musical files using LilyPond. -The class hierarchy mirrors the LilyPond grammar as published in -http://lilypond.org/doc/v2.14/Documentation/notation/lilypond-grammar - -**Targeted LilyPond version: 2.24 (December 2022).** - -Output is written for LilyPond 2.24 and checked against it. LilyPond's input -syntax changes between stable releases, so a construct taken from the v2.14 -grammar above is not necessarily still valid: `\\markuplines` became -`\\markuplist` in 2.16, tempo ranges moved from `70~100` to `70-100` in 2.18, -and most bar line names were reworked in 2.18 and again in 2.23. When adding -or changing output, check it against a real LilyPond 2.24 run, and use -LilyPond's own `convert-ly` (its `python/convertrules.py` is the authoritative -list of syntax changes) to find anything left over from an older grammar. - -The policy is to support back roughly four years from a music21 release, so -raise this target when 2.24 falls outside that window. +The class hierarchy mirrors the grammar in +https://lilypond.org/doc/v2.14/Documentation/notation/lilypond-grammar +(the last LilyPond to publish one is v2.19; since then it is `lily/parser.yy`). + +Output is written for LilyPond 2.24 and checked against it. Aim to support +3-4 years of LilyPond. ''' from __future__ import annotations From f556d1ac6520b81417c7eca13e9864c3037a6d5e Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 6 Aug 2026 16:37:01 -1000 Subject: [PATCH 3/4] Cut history and prior-bug notes from the docs Version markers keep only what a user acts on. AI-assisted (Claude) --- music21/lily/lilyObjects.py | 15 +++++---------- music21/lily/translate.py | 14 +++++--------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/music21/lily/lilyObjects.py b/music21/lily/lilyObjects.py index 62d42e53c..e91d3eb7e 100644 --- a/music21/lily/lilyObjects.py +++ b/music21/lily/lilyObjects.py @@ -46,8 +46,7 @@ class LyObject(prebase.ProtoM21Object): >>> lyo.stringOutput() '' - * Changed in v11: `stringOutput()` always returns a `str`; the subclasses that - returned None for empty contents now return `''`. + * Changed in v11: `stringOutput()` always returns a `str`, never None. ''' supportedClasses: list[str] = [] # ordered list of classes to support m21toLy: dict[str, dict[str, str]] = {} @@ -1355,10 +1354,7 @@ class LyPrefixCompositeMusic(LyObject): modeChanging, modeChangingWith, relative, rhythmed - The 'tuplet' fraction is the LilyPond `\tuplet` fraction, actual/normal: - `3/2` means three notes in the time of two. (The v2.14 grammar below - spells this `\times`, whose fraction is the inverse; `\tuplet` replaced it - in LilyPond 2.18.) + The 'tuplet' fraction is actual/normal: `3/2` is three notes in the time of two. prefix_composite_music: generic_prefix_music_scm | "\context" @@ -1468,7 +1464,7 @@ class LyModeChangingHead(LyObject): >>> print(l2.stringOutput()) \chords - 'note' has no context-creating shorthand in LilyPond, so it always gives `\notemode`: + Mode 'note' always gives `\notemode`: >>> l3 = lily.lilyObjects.LyModeChangingHead(hasContext=False, mode='note') >>> print(l3.stringOutput()) @@ -1858,7 +1854,7 @@ def stringOutput(self) -> str: argOut = arg.stringOutput() if isinstance(arg, LyObject) else str(arg) return self.backslash + ct + ' ' + argOut elif ct == '[': - # \[ and \] are ligature brackets; manual beams are plain [ and ] + # ligature brackets; manual beams are plain [ and ] return self.backslash + '[ ' elif ct == ']': return self.backslash + '] ' @@ -1884,7 +1880,7 @@ def __init__(self, def stringOutput(self) -> str: ct = self.commandType if ct == '~': - # E_TILDE: the pes-or-flexa ligature event, not a tie (a tie is a bare ~) + # E_TILDE, the pes-or-flexa ligature event return self.backslash + '~ ' elif ct == 'mark-default': return self.backslash + 'mark ' + self.backslash + 'default ' @@ -2242,7 +2238,6 @@ def stringOutput(self) -> str: elif mli is None: # pragma: no cover raise LilyObjectsException('need a markup list or identifier') else: - # \markuplines was renamed \markuplist in LilyPond 2.16 return self.backslash + 'markuplist ' + mli.stringOutput() diff --git a/music21/lily/translate.py b/music21/lily/translate.py index 46dac07eb..35e87bed4 100644 --- a/music21/lily/translate.py +++ b/music21/lily/translate.py @@ -141,8 +141,7 @@ class LilypondConverter: 'half-flat': 'eh', } - # bar line names as defined by LilyPond 2.18 and later; unknown names - # are silently drawn as nothing. + # bar line names as defined in LilyPond's scm/lily/bar-line.scm barlineDict = {'regular': '|', 'dotted': ';', 'dashed': '!', @@ -1716,8 +1715,7 @@ def lyEmbeddedScmFromMetronomeMark(self, mm: tempo.MetronomeMark) -> lyo.LyEmbed >>> conv.lyEmbeddedScmFromMetronomeMark(mm) is None True - * New in v11: MetronomeMark objects are now written out when - converting a Stream to LilyPond; previously they were silently dropped. + * New in v11. ''' if mm.number is None: return None @@ -1762,8 +1760,8 @@ def setContextForTimeFraction( ) -> lyo.LyMusicList: r''' Explicitly starts a new context for scaled music (tuplets, etc.) for the - given LilyPond `\tuplet` fraction, actual/normal: 5/4 means five notes in - the time of four. Either part may be an int or a string. + given fraction, actual/normal: 5/4 is five notes in the time of four. + Either part may be an int or a string. Returns an lpMusicList object contained in an lpSequentialMusic object in an lpPrefixCompositeMusic object which sets the tuplet to a particular @@ -1793,9 +1791,7 @@ def setContextForTimeFraction( >>> lpc.context.getParent().getParent().getParent() is lyTop True - * Changed in v11: emits `\tuplet actual/normal` rather than the - `\times normal/actual` removed after LilyPond 2.16; the arguments - are correspondingly swapped. + * Changed in v11: emits `\tuplet`; the arguments are now actual, normal. ''' fraction = str(actual) + '/' + str(normal) lpMusicList = lyo.LyMusicList() From dae4ea3a0a983d7104daf65da89586869b6530cd Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 6 Aug 2026 16:39:49 -1000 Subject: [PATCH 4/4] Add a writing-docs skill House style for docstrings, comments and version markers: the length target, keeping fixed bugs in the commit message rather than the code, and regression cases belonging in unittests rather than doctests. AI-assisted (Claude) --- .agents/skills/writing-docs/SKILL.md | 80 ++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 .agents/skills/writing-docs/SKILL.md diff --git a/.agents/skills/writing-docs/SKILL.md b/.agents/skills/writing-docs/SKILL.md new file mode 100644 index 000000000..6bc145c82 --- /dev/null +++ b/.agents/skills/writing-docs/SKILL.md @@ -0,0 +1,80 @@ +--- +name: writing-docs +description: >- + House style for docstrings, code comments, and `Changed in`/`New in` version + markers in music21. Use whenever you write or edit a docstring or comment, add + a version marker, or decide where a bug fix's test belongs. Covers the length + target, the rule against narrating bugs you just fixed, and why regression + cases go in unittests rather than doctests. +--- + +# Writing docs and comments + +## Length + +Aim for about 40% of the length an LLM writes by default. Cut qualifiers, +restatements, and the sentence that explains the sentence before it. A comment +earning its place says something the code cannot. + +## Say what is, not what was or what not to do + +Describe current behavior. Do not narrate the bug you just fixed, the old +spelling of an API, or when upstream changed something. + +```python +# yes +# ligature brackets; manual beams are plain [ and ] +return self.backslash + '[ ' + +# no +# \[ used to be emitted for beams, which was wrong -- LilyPond renamed +# this in 2.16 and it silently produced nothing +``` + +The commit message is where a fixed bug belongs: why it was wrong, how it was +found, what it broke. That costs nothing until someone runs `git log`, and +`git blame` leads them there from the line itself. + +The exception is a mistake that is likely to recur — a genuine trap that the +next person would otherwise walk into. Rare. Prefer stating the rule positively +even then. + +## Version markers + +`* Changed in v[X]: one line.` or `* New in v[X].` Only for user-facing changes +to the public interface: a signature, a return type, an output format. Keep to +what a reader must act on; drop the before-picture. + +``` +* Changed in v11: `stringOutput()` always returns a `str`, never None. +* Changed in v11: emits `\tuplet`; the arguments are now actual, normal. +* New in v11. +``` + +A plain bug fix — code now does what it always claimed — gets no marker and no +doctest. It goes in the commit message. + +See the `bump-version` skill for which digit to change and the odd/even +convention. + +## Doctests are not regression tests + +Doctests are documentation that happens to be verified. Every example must earn +its place by teaching the reader something about how to use the object. + +Regression cases go in the module's `Test(unittest.TestCase)` class, where a +comment or the method name can name the issue: + +```python +def testMetronomeMarkWrittenInStream(self): + # https://github.com/cuthbertLab/music21/issues/1852 + ... +``` + +So: a fix for a crash on an edge case, a check that some input no longer +produces invalid output, an assertion tied to an issue number — unittest. An +example a user would want to read — doctest. + +Naming the guarded bug **is** appropriate in a unittest; that is what the test +is for. The rule against narrating old bugs applies to docstrings and to +comments in shipping code, not to tests.