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. diff --git a/music21/lily/lilyObjects.py b/music21/lily/lilyObjects.py index 77eadfa0a..e91d3eb7e 100644 --- a/music21/lily/lilyObjects.py +++ b/music21/lily/lilyObjects.py @@ -13,18 +13,26 @@ local computer, can automatically generate .pdf, .png, and .svg versions of musical files using LilyPond. -The Grammar for LilyPond comes from -http://lilypond.org/doc/v2.14/Documentation/notation/lilypond-grammar +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 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 +46,25 @@ class LyObject(prebase.ProtoM21Object): >>> lyo.stringOutput() '' + * Changed in v11: `stringOutput()` always returns a `str`, never None. ''' - 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 +75,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 +94,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 +109,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 +118,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 +148,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 +220,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 +246,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 +257,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 +286,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 +313,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 +342,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 +362,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 +380,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 +405,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 +431,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 +461,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 +499,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 +530,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 +541,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 +576,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 +590,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 +612,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 +642,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 +652,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 +678,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 +707,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 +717,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 +747,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 +771,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 +787,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 +809,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 +835,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 +857,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 +880,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 +896,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: @@ -877,33 +931,34 @@ class LyTempoEvent(LyObject): A steno_duration paired with a single bpm scalar (and no tempoRange) is the common case for a music21 MetronomeMark, e.g. quarter = 87: - >>> steno = lily.lilyObjects.LyStenoDuration('4') + >>> steno = lily.lilyObjects.LyStenoDuration(4) >>> lte = lily.lilyObjects.LyTempoEvent(stenoDuration=steno, scalar=87) >>> str(lte) '\\tempo 4 = 87' - More complex, with a tempo range. Note that steno_duration takes a - Lilypond duration number such as '4' for a quarter note, not the - English name 'quarter': + More complex, with a tempo range: - >>> steno = lily.lilyObjects.LyStenoDuration('4') >>> tempoRange = lily.lilyObjects.LyTempoRange(70, 100) >>> lte = lily.lilyObjects.LyTempoEvent(tempoRange=tempoRange, stenoDuration=steno) >>> str(lte) - '\\tempo 4 = 70~100 ' + '\\tempo 4 = 70-100 ' >>> lte.scalar = 85 >>> str(lte) - '\\tempo 85 4 = 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 @@ -933,24 +988,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: @@ -961,11 +1019,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: @@ -974,14 +1032,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() @@ -1000,13 +1068,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: @@ -1031,12 +1103,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' @@ -1059,12 +1131,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: @@ -1078,15 +1150,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: @@ -1101,13 +1177,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: @@ -1121,14 +1201,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 @@ -1141,20 +1224,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: @@ -1174,12 +1262,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: @@ -1228,13 +1319,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) @@ -1244,13 +1335,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 @@ -1259,10 +1350,12 @@ class LyPrefixCompositeMusic(LyObject): r''' type must be specified. Should be one of: - scheme, context, new, times, repeated, transpose, + scheme, context, new, tuplet, repeated, transpose, modeChanging, modeChangingWith, relative, rhythmed + The 'tuplet' fraction is actual/normal: `3/2` is three notes in the time of two. + prefix_composite_music: generic_prefix_music_scm | "\context" simple_string @@ -1274,7 +1367,7 @@ class LyPrefixCompositeMusic(LyObject): optional_id optional_context_mod music - | "\times" fraction music + | "\tuplet" fraction music | repeated_music | "\transpose" pitch_also_in_chords @@ -1288,14 +1381,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 @@ -1313,7 +1415,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) @@ -1325,8 +1427,8 @@ def stringOutput(self): c += str(self.optionalContextMod) + ' ' c += str(self.music) + ' ' return c - elif myType == 'times': - return self.backslash + 'times ' + str(self.fraction) + ' ' + str(self.music) + ' ' + elif myType == 'tuplet': + return self.backslash + 'tuplet ' + str(self.fraction) + ' ' + str(self.music) + ' ' elif myType == 'repeated': return str(self.repeatedMusic) elif myType == 'transpose': @@ -1362,24 +1464,30 @@ class LyModeChangingHead(LyObject): >>> print(l2.stringOutput()) \chords + Mode 'note' 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): @@ -1387,11 +1495,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() @@ -1400,17 +1510,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 + ' ' @@ -1419,19 +1529,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): @@ -1441,12 +1557,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 + ' ' @@ -1457,14 +1573,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]) @@ -1496,17 +1612,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': @@ -1516,6 +1634,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): @@ -1523,21 +1643,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: @@ -1549,12 +1674,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 ' @@ -1578,8 +1705,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 @@ -1589,7 +1723,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: @@ -1622,7 +1756,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__() @@ -1630,7 +1768,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) + ' ' @@ -1641,14 +1779,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, '> ']) @@ -1668,13 +1806,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 @@ -1696,41 +1834,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 == '[': + # 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 return self.backslash + '~ ' elif ct == 'mark-default': return self.backslash + 'mark ' + self.backslash + 'default ' @@ -1739,29 +1889,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) @@ -1774,34 +1926,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: @@ -1814,13 +1966,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 @@ -1830,11 +1982,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) + ' ' @@ -1846,12 +1998,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): @@ -1862,12 +2014,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 @@ -1885,12 +2037,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 + ' ' @@ -1900,20 +2052,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: @@ -1923,11 +2078,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: @@ -1937,11 +2092,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: @@ -1963,13 +2118,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 @@ -1986,29 +2141,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): @@ -2016,13 +2177,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 + ' ' @@ -2032,24 +2193,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: @@ -2057,81 +2221,91 @@ 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() + 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) + ' ' @@ -2152,28 +2326,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 4105e87fd..35e87bed4 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 tempo from music21 import variant @@ -59,7 +66,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) @@ -68,7 +75,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 @@ -92,11 +99,11 @@ def makeLettersOnlyId(inputString): class LilypondConverter: fictaDef = ( r''' - ficta = #(define-music-function (parser location) () #{ \once \set suggestAccidentals = ##t #}) + ficta = #(define-music-function () () #{ \once \set suggestAccidentals = ##t #}) '''.lstrip()) colorDef = ( r''' - color = #(define-music-function (parser location color) (string?) #{ + color = #(define-music-function (color) (string?) #{ \once \override NoteHead.color = #(x11-color color) \once \override Stem.color = #(x11-color color) \once \override Rest.color = #(x11-color color) @@ -134,46 +141,47 @@ class LilypondConverter: 'half-flat': 'eh', } + # bar line names as defined in LilyPond's scm/lily/bar-line.scm 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 @@ -197,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( @@ -238,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 @@ -250,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 @@ -258,7 +266,7 @@ def textFromMusic21Object(self, m21ObjectIn): >>> print(lily.translate.LilypondConverter().textFromMusic21Object(n)) \version "2..." \include "lilypond-book-preamble.ly" - color = #(define-music-function (parser location color) (string?) #{ + color = #(define-music-function (color) (string?) #{ \once \override NoteHead.color = #(x11-color color) \once \override Stem.color = #(x11-color color) \once \override Rest.color = #(x11-color color) @@ -278,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. @@ -286,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 @@ -325,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) @@ -354,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) @@ -382,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) @@ -419,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) @@ -463,8 +465,7 @@ def lyPartsAndOssiaInitFromScore(self, scoreIn): \override TupletBracket.bracket-visibility = ##f \override TupletNumber.stencil = ##f \override Clef.transparent = ##t - \override OctavateEight.transparent = ##t - \consists "Default_bar_line_engraver" + \override ClefModifier.transparent = ##t } { \stopStaff s1 s1 s1 s1 } \new Staff = romepa @@ -477,8 +478,7 @@ def lyPartsAndOssiaInitFromScore(self, scoreIn): \override TupletBracket.bracket-visibility = ##f \override TupletNumber.stencil = ##f \override Clef.transparent = ##t - \override OctavateEight.transparent = ##t - \consists "Default_bar_line_engraver" + \override ClefModifier.transparent = ##t } { \stopStaff s1 s1 s1 s1 } \new Staff = pb { \stopStaff s1 s1 s1 s1 } @@ -492,8 +492,7 @@ def lyPartsAndOssiaInitFromScore(self, scoreIn): \override TupletBracket.bracket-visibility = ##f \override TupletNumber.stencil = ##f \override Clef.transparent = ##t - \override OctavateEight.transparent = ##t - \consists "Default_bar_line_engraver" + \override ClefModifier.transparent = ##t } { \stopStaff s1 s1 s1 s1 } \new Staff = romepb @@ -506,14 +505,13 @@ def lyPartsAndOssiaInitFromScore(self, scoreIn): \override TupletBracket.bracket-visibility = ##f \override TupletNumber.stencil = ##f \override Clef.transparent = ##t - \override OctavateEight.transparent = ##t - \consists "Default_bar_line_engraver" + \override ClefModifier.transparent = ##t } { \stopStaff s1 s1 s1 s1 } ''' lpMusicList = lyo.LyMusicList() - musicList = [] + musicList: list[lyo.LyObject|str] = [] lpMusic = r'{ \stopStaff %s}' for p in scoreIn.parts: @@ -526,7 +524,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: @@ -548,8 +546,7 @@ def lyPartsAndOssiaInitFromScore(self, scoreIn): r'\override TupletBracket.bracket-visibility = ##f', r'\override TupletNumber.stencil = ##f', r'\override Clef.transparent = ##t', - r'\override OctavateEight.transparent = ##t', - r'\consists "Default_bar_line_engraver"', + r'\override ClefModifier.transparent = ##t', ] optionalContextMod = lyo.LyContextModification(contextModList) lpPrefixCompositeMusicVariant.optionalContextMod = optionalContextMod @@ -559,7 +556,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. @@ -607,7 +604,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: @@ -674,7 +675,7 @@ def lyGroupedMusicListFromScoreWithParts(self, scoreIn, scoreInit=None): ''' - compositeMusicList = [] + compositeMusicList: list[lyo.LyObject|str] = [] lpGroupedMusicList = lyo.LyGroupedMusicList() lpSimultaneousMusic = lyo.LySimultaneousMusic() @@ -700,7 +701,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 @@ -727,9 +733,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) @@ -737,6 +743,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) @@ -752,7 +760,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. @@ -779,15 +787,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 = ' _ ' @@ -808,7 +811,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 @@ -829,7 +836,7 @@ def lySequentialMusicFromStream(self, streamIn, beforeMatter=None): } ''' - musicList = [] + musicList: list[lyo.LyObject|str] = [] lpMusicList = lyo.LyMusicList(contents=musicList) lpSequentialMusic = lyo.LySequentialMusic(musicList=lpMusicList, @@ -847,11 +854,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 @@ -883,7 +890,7 @@ def lyPrefixCompositeMusicFromStream( compositeMusicType = type optionalId = None - contextModList = [] + contextModList: list[str] = [] c = streamIn.classes if contextType is None: @@ -917,10 +924,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, @@ -929,7 +933,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 @@ -976,18 +980,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) @@ -1005,7 +1009,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() @@ -1019,8 +1023,7 @@ def appendObjectsToContextFromStream(self, streamObject): lp2MusicList.contents = musicList2 - contextObject = self.context - currentMusicList = contextObject.contents + currentMusicList = self.contextContents() currentMusicList.append(lp2GroupedMusicList) lp2GroupedMusicList.setParent(self.context) @@ -1028,7 +1031,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 @@ -1037,28 +1040,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 @@ -1076,51 +1073,48 @@ 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 'MetronomeMark' in c: + elif isinstance(thisObject, tempo.MetronomeMark): lyObject = self.lyEmbeddedScmFromMetronomeMark(thisObject) if lyObject is not None: 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. @@ -1152,7 +1146,7 @@ def appendContextFromNoteOrRest(self, noteOrRest): >>> print(lpc.context) cis' 4 - \times 2/3 { dis' 8 + \tuplet 3/2 { dis' 8 e' 8 f' 8 } @@ -1181,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. @@ -1217,7 +1211,7 @@ def appendContextFromChord(self, chord): >>> print(lpc.context) < c' e' g' > 4 - \times 2/3 { < d' fis' a' > 8 + \tuplet 3/2 { < d' fis' a' > 8 < d' f' g' > 8 < c' e' g' c'' > 8 } @@ -1226,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:: @@ -1273,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: @@ -1286,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: @@ -1321,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. @@ -1374,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). @@ -1406,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() @@ -1425,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: @@ -1432,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: @@ -1458,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 @@ -1480,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,, @@ -1520,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: @@ -1587,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 @@ -1633,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 @@ -1652,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) @@ -1670,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 @@ -1705,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 @@ -1718,7 +1727,7 @@ def lyEmbeddedScmFromMetronomeMark(self, mm: tempo.MetronomeMark) -> lyo.LyEmbed lpEmbeddedScm.content = tempoEvent.stringOutput() + lpEmbeddedScm.newlineIndent 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. @@ -1737,20 +1746,25 @@ def setContextForTupletStart(self, inObj): if not inObj.duration.tuplets: return None elif inObj.duration.tuplets[0].type == 'start': - numerator = str(int(inObj.duration.tuplets[0].tupletNormal[0])) - denominator = str(int(inObj.duration.tuplets[0].tupletActual[0])) - lpMusicList = self.setContextForTimeFraction(numerator, denominator) + actual = str(int(inObj.duration.tuplets[0].tupletActual[0])) + normal = str(int(inObj.duration.tuplets[0].tupletNormal[0])) + lpMusicList = self.setContextForTimeFraction(actual, normal) return lpMusicList else: return None - def setContextForTimeFraction(self, numerator, denominator): + def setContextForTimeFraction( + self, + actual: int|str, + normal: 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) + Explicitly starts a new context for scaled music (tuplets, etc.) for the + 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 times object to a particular + in an lpPrefixCompositeMusic object which sets the tuplet to a particular fraction. >>> lpc = lily.translate.LilypondConverter() @@ -1767,37 +1781,35 @@ def setContextForTimeFraction(self, numerator, denominator): >>> lpc.context.getParent() >>> lpc.context.getParent().getParent() - + >>> lpc.context.getParent().getParent().fraction '5/4' >>> lpc.context.getParent().getParent().type - 'times' + 'tuplet' >>> lpc.context.getParent().getParent().getParent() - + >>> lpc.context.getParent().getParent().getParent() is lyTop True + + * Changed in v11: emits `\tuplet`; the arguments are now actual, normal. ''' - fraction = str(numerator) + '/' + str(denominator) + fraction = str(actual) + '/' + str(normal) lpMusicList = lyo.LyMusicList() lpSequentialMusic = lyo.LySequentialMusic(musicList=lpMusicList) # technically needed, but we can speed things up # lpGroupedMusicList = lyo.LyGroupedMusicList(sequentialMusic=lpSequentialMusic) # lpCompositeMusic = lyo.LyCompositeMusic(groupedMusicList=lpGroupedMusicList) # lpMusic = lyo.LyMusic(compositeMusic=lpCompositeMusic) - lpPrefixCompositeMusic = lyo.LyPrefixCompositeMusic(type='times', + lpPrefixCompositeMusic = lyo.LyPrefixCompositeMusic(type='tuplet', 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 ''' @@ -1805,15 +1817,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 @@ -1826,8 +1841,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] @@ -1869,15 +1884,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''' @@ -1933,7 +1949,7 @@ def lyPrefixCompositeMusicFromRelatedVariants(self, >>> print(lpc.lyPrefixCompositeMusicFromRelatedVariants(variantList, ... activeSite=activeSite)[0]) - \new Staff = london... { { \times 1/2 {\startStaff \clef "treble" + \new Staff = london... { { \tuplet 2/1 {\startStaff \clef "treble" a' 4 a' 4 a' 4 @@ -1977,12 +1993,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)) @@ -1991,9 +2008,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] @@ -2013,9 +2032,9 @@ def findOffsetOfFirstNonSpacerElement(inputStream): ####################### - musicList = [] + musicList: list[lyo.LyObject|str] = [] highestOffsetSoFar = 0.0 - longestVariant = None + longestVariant = variantList[-1] self.variantMode = True @@ -2056,12 +2075,13 @@ def findOffsetOfFirstNonSpacerElement(inputStream): replacedElementsLength = vStripped.replacementQuarterLength variantLength = vStripped.containedHighestTime - firstOffset + lpOssiaMusicVariant: lyo.LyObject if variantLength != replacedElementsLength: - numerator, denominator = common.decimalToTuplet( + normal, actual = common.decimalToTuplet( replacedElementsLength / variantLength) - fraction = str(numerator) + '/' + str(denominator) + fraction = str(actual) + '/' + str(normal) lpOssiaMusicVariantPreFraction = self.lyOssiaMusicFromVariant(vStripped) - lpVariantTuplet = lyo.LyPrefixCompositeMusic(type='times', + lpVariantTuplet = lyo.LyPrefixCompositeMusic(type='tuplet', fraction=fraction, music=lpOssiaMusicVariantPreFraction) @@ -2093,10 +2113,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''' @@ -2138,9 +2160,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: @@ -2164,7 +2188,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] @@ -2186,9 +2210,9 @@ def lyPrefixCompositeMusicFromVariant(self, self.variantMode = True if variantLength != replacedElementsLength: - numerator, denominator = common.decimalToTuplet(replacedElementsLength / variantLength) - fraction = str(numerator) + '/' + str(denominator) - lpVariantTuplet = lyo.LyPrefixCompositeMusic(type='times', + normal, actual = common.decimalToTuplet(replacedElementsLength / variantLength) + fraction = str(actual) + '/' + str(normal) + lpVariantTuplet = lyo.LyPrefixCompositeMusic(type='tuplet', fraction=fraction, music=lpOssiaMusicVariant) lpInternalSequentialMusic = lyo.LySequentialMusic(musicList=lpVariantTuplet) @@ -2248,7 +2272,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 @@ -2270,7 +2294,7 @@ def lyOssiaMusicFromVariant(self, variantIn): } ''' - musicList = [] + musicList: list[lyo.LyObject|str] = [] lpMusicList = lyo.LyMusicList(contents=musicList) lpOssiaMusic = lyo.LyOssiaMusic(musicList=lpMusicList) @@ -2289,7 +2313,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 @@ -2340,7 +2368,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 @@ -2386,7 +2414,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 @@ -2424,7 +2452,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. @@ -2444,8 +2472,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. @@ -2487,7 +2520,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. @@ -2497,7 +2530,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) @@ -2517,7 +2550,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. @@ -2538,7 +2571,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 @@ -2553,7 +2586,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. @@ -2563,7 +2596,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) @@ -2589,7 +2622,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 @@ -2645,7 +2677,6 @@ def testMetronomeMark(self): def testMetronomeMarkWrittenInStream(self): # https://github.com/cuthbertLab/music21/issues/1852 - from music21 import meter keysig = key.Key('a-') mm = tempo.MetronomeMark(number=87, referent=note.Note(type='quarter')) timesig = meter.TimeSignature('3/4') @@ -2675,7 +2706,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()