From 9275d378b2cdbc04dffa05fd0a9af7b0fe54cd89 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Mon, 5 Dec 2022 15:02:43 -1000 Subject: [PATCH 01/16] Upgrade mypy to 0.991 All errors fixed in first commit Second commit for the notes --- .github/workflows/maincheck.yml | 5 ++- music21/base.py | 1 - music21/chord/__init__.py | 59 +++++++++++++++++++++++------- music21/common/classTools.py | 1 - music21/converter/subConverters.py | 2 +- music21/key.py | 2 +- music21/layout.py | 10 ++--- music21/musicxml/m21ToXml.py | 2 +- music21/stream/base.py | 19 +++++----- music21/test/testRunner.py | 2 +- music21/test/test_metadata.py | 3 +- music21/voiceLeading.py | 32 +++++++++++----- requirements_dev.txt | 2 +- 13 files changed, 93 insertions(+), 47 deletions(-) diff --git a/.github/workflows/maincheck.yml b/.github/workflows/maincheck.yml index fe315a38c9..ab4f0f87c5 100644 --- a/.github/workflows/maincheck.yml +++ b/.github/workflows/maincheck.yml @@ -45,6 +45,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install wheel pip install -r requirements.txt pip install -r requirements_dev.txt - name: Install music21 in editable mode @@ -67,6 +68,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install wheel pip install -r requirements.txt pip install -r requirements_dev.txt - name: PEP8 with flake8 @@ -81,10 +83,11 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.10.6' # pinned until mypy 0.990 is released + python-version: '3.10' cache: 'pip' - name: Install dependencies run: | + pip install wheel python -m pip install -r requirements.txt python -m pip install -r requirements_dev.txt - name: Type-check all modules with mypy diff --git a/music21/base.py b/music21/base.py index c4fbddd490..457fad51e0 100644 --- a/music21/base.py +++ b/music21/base.py @@ -1304,7 +1304,6 @@ def getContextByClass( ) -> Music21Object | None: return None # until Astroid #1015 - def getContextByClass( self, className: type[_M21T] | str | None, diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index 7fae0a5c78..ea62caa61f 100644 --- a/music21/chord/__init__.py +++ b/music21/chord/__init__.py @@ -214,6 +214,8 @@ def _add_core_or_init(self, Does not clear any caches. Also requires that notes be iterable. + + Changed in v9: incorrect arguments raise TypeError ''' # quickDuration specifies whether the duration object for the chord # should be taken from the first note of the list. @@ -252,8 +254,7 @@ def _add_core_or_init(self, self._notes.append(note.Note(n)) # self._notes.append({'pitch':music21.pitch.Pitch(n)}) else: - # TODO: v8 raise TypeError - raise ChordException(f'Could not process input argument {n}') + raise TypeError(f'Could not process input argument {n}') for n in self._notes: # noinspection PyProtectedMember @@ -1116,14 +1117,47 @@ def add( if runSort: self.sortAscending(inPlace=True) + @overload def annotateIntervals( - self, + self: _ChordType, *, - inPlace=True, - stripSpecifiers=True, - sortPitches=True, - returnList=False - ): + inPlace: bool = False, + stripSpecifiers: bool = True, + sortPitches: bool = True, + returnList: t.Literal[True] + ) -> list[str]: + pass + + @overload + def annotateIntervals( + self: _ChordType, + *, + inPlace: t.Literal[True], + stripSpecifiers: bool = True, + sortPitches: bool = True, + returnList: t.Literal[False] = False + ) -> None: + pass + + @overload + def annotateIntervals( + self: _ChordType, + *, + inPlace: t.Literal[False] = False, + stripSpecifiers: bool = True, + sortPitches: bool = True, + returnList: t.Literal[False] = False + ) -> _ChordType: + pass + + def annotateIntervals( + self: _ChordType, + *, + inPlace: bool = False, + stripSpecifiers: bool = True, + sortPitches: bool = True, + returnList: bool = False + ) -> _ChordType | None | list[str]: # noinspection PyShadowingNames ''' Add lyrics to the chord that show the distance of each note from @@ -1189,7 +1223,6 @@ def annotateIntervals( >>> [ly.text for ly in c.lyrics] ['5', '3'] ''' - # TODO: -- decide, should inPlace be False like others? # make a copy of self for reducing pitches, but attach to self c = copy.deepcopy(self) # this could be an option @@ -1209,19 +1242,19 @@ def annotateIntervals( notation = str(i.diatonic.generic.semiSimpleUndirected) lyricsList.append(notation) - if stripSpecifiers is True and sortPitches is True: + if stripSpecifiers and sortPitches: lyricsList.sort(reverse=True) - if returnList is True: + if returnList: return lyricsList for notation in lyricsList: - if inPlace is True: + if inPlace: self.addLyric(notation) else: c.addLyric(notation) - if inPlace is False: + if not inPlace: return c def areZRelations(self: _ChordType, other: _ChordType) -> bool: diff --git a/music21/common/classTools.py b/music21/common/classTools.py index e62afc0c60..865b8dcdb0 100644 --- a/music21/common/classTools.py +++ b/music21/common/classTools.py @@ -75,7 +75,6 @@ def isNum(usrData: t.Any) -> t.TypeGuard[numbers.Rational]: ''' # noinspection PyBroadException try: - # TODO: this may have unexpected consequences: find dummy = usrData + 0 if usrData is not True and usrData is not False: return True diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index 3b6e1f2d6e..f978b03fda 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -1501,7 +1501,7 @@ def parseFile(self, # environLocal.printDebug(['partStr', len(partStr)]) mdw.addString(partStr) else: - if fp.is_dir: + if fp.is_dir(): mdd = musedataModule.MuseDataDirectory(fp) fpList = mdd.getPaths() elif not common.isListLike(fp): diff --git a/music21/key.py b/music21/key.py index e21b9817dc..1590194ae4 100644 --- a/music21/key.py +++ b/music21/key.py @@ -168,7 +168,7 @@ def sharpsToPitch(sharpCount): def pitchToSharps(value: str | pitch.Pitch | note.Note, - mode: str = None) -> int: + mode: str | None = None) -> int: ''' Given a pitch string or :class:`music21.pitch.Pitch` or :class:`music21.note.Note` object, diff --git a/music21/layout.py b/music21/layout.py index f04b52abfc..e8c65940c8 100644 --- a/music21/layout.py +++ b/music21/layout.py @@ -430,13 +430,13 @@ def __init__(self, name: str | None = None, barTogether: t.Literal[True, False, None, 'Mensurstrich'] = True, abbreviation: str | None = None, - symbol: t.Literal['bracket', 'line', 'brace', 'square'] = None, + symbol: t.Literal['bracket', 'line', 'brace', 'square'] | None = None, **keywords): super().__init__(*spannedElements, **keywords) self.name = name or abbreviation # if this group has a name self.abbreviation = abbreviation - self._symbol = None # Choices: bracket, line, brace, square + self._symbol: t.Literal['bracket', 'line', 'brace', 'square'] | None = None self.symbol = symbol # determines if barlines are grouped through; this is group barline # in musicxml @@ -475,14 +475,14 @@ def _setBarTogether(self, value: t.Literal[True, False, None, 'Mensurstrich', 'y 'Mensurstrich' ''') - def _getSymbol(self): + def _getSymbol(self) -> t.Literal['bracket', 'line', 'brace', 'square'] | None: return self._symbol - def _setSymbol(self, value): + def _setSymbol(self, value: t.Literal['bracket', 'line', 'brace', 'square'] | None): if value is None or str(value).lower() == 'none': self._symbol = None elif value.lower() in ['brace', 'line', 'bracket', 'square']: - self._symbol = value.lower() + self._symbol = t.cast(t.Literal['bracket', 'line', 'brace', 'square'], value.lower()) else: raise StaffGroupException(f'the symbol value {value} is not acceptable') diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index c52caeb5b9..14ba456115 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -4342,7 +4342,7 @@ def pitchToXml(self, p: pitch.Pitch): def unpitchedToXml(self, up: note.Unpitched, noteIndexInChord: int = 0, - chordParent: chord.ChordBase = None) -> Element: + chordParent: chord.ChordBase | None = None) -> Element: # noinspection PyShadowingNames ''' Convert an :class:`~music21.note.Unpitched` to a diff --git a/music21/stream/base.py b/music21/stream/base.py index 3d7f57d2f3..592d843242 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -4915,7 +4915,10 @@ def optionalAddRest(): return out - def measureOffsetMap(self, classFilterList=None): + def measureOffsetMap( + self, + classFilterList: list[t.Type] | list[str] | tuple[t.Type] | tuple[str] = (Measure,) + ): ''' If this Stream contains Measures, returns an OrderedDict whose keys are the offsets of the start of each measure @@ -4996,15 +4999,10 @@ def measureOffsetMap(self, classFilterList=None): see important examples in testMeasureOffsetMap() and testMeasureOffsetMapPostTie() ''' - if classFilterList is None: - classFilterList = [Measure] - elif not isinstance(classFilterList, (list, tuple)): - classFilterList = [classFilterList] - # environLocal.printDebug(['calling measure offsetMap()']) # environLocal.printDebug([classFilterList]) - offsetMap = {} + offsetMap: dict[float | Fraction, list[Measure]] = {} # first, try to get measures # this works best of this is a Part or Score if Measure in classFilterList or 'Measure' in classFilterList: @@ -5017,7 +5015,7 @@ def measureOffsetMap(self, classFilterList=None): # try other classes for className in classFilterList: - if className in [Measure or 'Measure']: # do not redo + if className in (Measure, 'Measure'): # do not redo continue for e in self.getElementsByClass(className): # environLocal.printDebug(['calling measure offsetMap(); e:', e]) @@ -5025,10 +5023,11 @@ def measureOffsetMap(self, classFilterList=None): # long time to process # 'reverse' here is a reverse sort, where the oldest objects # are returned first - m = e.getContextByClass('Measure') # , sortByCreationTime='reverse') - if m is None: # pragma: no cover + maybe_m = e.getContextByClass(Measure) # , sortByCreationTime='reverse') + if maybe_m is None: # pragma: no cover # hard to think of a time this would happen...But... continue + m = maybe_m # assuming that the offset returns the proper offset context # this is, the current offset may not be the stream that # contains this Measure; its current activeSite diff --git a/music21/test/testRunner.py b/music21/test/testRunner.py index 8a7961b59e..851e0fbc8a 100644 --- a/music21/test/testRunner.py +++ b/music21/test/testRunner.py @@ -83,7 +83,7 @@ def addDocAttrTestsToSuite(suite, suite.addTest(dtc) -def fixDoctests(doctestSuite): +def fixDoctests(doctestSuite: doctest._DocTestSuite) -> None: r''' Fix doctests so that addresses are sanitized. diff --git a/music21/test/test_metadata.py b/music21/test/test_metadata.py index fb53b999e9..4e515fff3c 100644 --- a/music21/test/test_metadata.py +++ b/music21/test/test_metadata.py @@ -2,6 +2,7 @@ from __future__ import annotations import re +import typing as t import unittest from music21 import converter @@ -198,7 +199,7 @@ def checkUniqueNamedItem( self, uniqueName: str, namespaceName: str, - contributorRole: str = None, + contributorRole: t.Optional[str] = None, valueType: type = metadata.Text): if ':' not in namespaceName: diff --git a/music21/voiceLeading.py b/music21/voiceLeading.py index 786b16e853..a5ff70f8d1 100644 --- a/music21/voiceLeading.py +++ b/music21/voiceLeading.py @@ -96,7 +96,15 @@ class VoiceLeadingQuartet(base.Music21Object): ''', } - def __init__(self, v1n1=None, v1n2=None, v2n1=None, v2n2=None, analyticKey=None, **keywords): + def __init__( + self, + v1n1: None | str | note.Note | pitch.Pitch = None, + v1n2: None | str | note.Note | pitch.Pitch = None, + v2n1: None | str | note.Note | pitch.Pitch = None, + v2n2: None | str | note.Note | pitch.Pitch = None, + analyticKey: key.Key | None = None, + **keywords + ): super().__init__(**keywords) if not intervalCache: # populate interval cache if not done yet @@ -199,7 +207,11 @@ def key(self, keyValue): ) self._key = keyValue - def _setVoiceNote(self, value, which): + def _setVoiceNote( + self, + value: None | str | note.Note | pitch.Pitch, + which: t.Literal['_v1n1', '_v1n2', '_v2n1', '_v2n2'] + ): if value is None: setattr(self, which, None) elif isinstance(value, str): @@ -218,10 +230,10 @@ def _setVoiceNote(self, value, which): f'not a valid note specification: {value!r}' ) from e - def _getV1n1(self): + def _getV1n1(self) -> None | note.Note: return self._v1n1 - def _setV1n1(self, value): + def _setV1n1(self, value: None | str | note.Note | pitch.Pitch): self._setVoiceNote(value, '_v1n1') v1n1 = property(_getV1n1, _setV1n1, doc=''' @@ -232,10 +244,10 @@ def _setV1n1(self, value): ''') - def _getV1n2(self): + def _getV1n2(self) -> None | note.Note: return self._v1n2 - def _setV1n2(self, value): + def _setV1n2(self, value: None | str | note.Note | pitch.Pitch): self._setVoiceNote(value, '_v1n2') v1n2 = property(_getV1n2, _setV1n2, doc=''' @@ -246,10 +258,10 @@ def _setV1n2(self, value): ''') - def _getV2n1(self): + def _getV2n1(self) -> None | note.Note: return self._v2n1 - def _setV2n1(self, value): + def _setV2n1(self, value: None | str | note.Note | pitch.Pitch): self._setVoiceNote(value, '_v2n1') v2n1 = property(_getV2n1, _setV2n1, doc=''' @@ -260,10 +272,10 @@ def _setV2n1(self, value): ''') - def _getV2n2(self): + def _getV2n2(self) -> None | note.Note: return self._v2n2 - def _setV2n2(self, value): + def _setV2n2(self, value: None | str | note.Note | pitch.Pitch): self._setVoiceNote(value, '_v2n2') v2n2 = property(_getV2n2, _setV2n2, doc=''' diff --git a/requirements_dev.txt b/requirements_dev.txt index 69e05e5f77..68aef77485 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,7 +3,7 @@ pylint>=2.15.4 flake8<6.0.0 flake8-quotes hatchling -mypy==0.982 +mypy coveralls scipy sphinx From 92b1b1c82a13f59edb93b94df17601626efad3e3 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Mon, 5 Dec 2022 15:12:06 -1000 Subject: [PATCH 02/16] cannot use a forward reference in definition --- music21/stream/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/music21/stream/base.py b/music21/stream/base.py index 592d843242..acafb3ce9a 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -4917,7 +4917,7 @@ def optionalAddRest(): def measureOffsetMap( self, - classFilterList: list[t.Type] | list[str] | tuple[t.Type] | tuple[str] = (Measure,) + classFilterList: list[t.Type] | list[str] | tuple[t.Type] | tuple[str] = ('Measure',) ): ''' If this Stream contains Measures, returns an OrderedDict @@ -4994,6 +4994,8 @@ def measureOffsetMap( Tenor Bass + Changed in v9: classFilterList must be a list or tuple of strings or Music21Objects + OMIT_FROM_DOCS see important examples in testMeasureOffsetMap() and From 60c7d6a7a2a7eff5f387fec5a244d90315cab0e0 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Mon, 5 Dec 2022 20:55:34 -1000 Subject: [PATCH 03/16] fix some issues --- music21/chord/__init__.py | 7 +- music21/common/numberTools.py | 2 +- music21/metadata/__init__.py | 17 ++- music21/musicxml/m21ToXml.py | 25 ++-- music21/repeat.py | 42 ++++--- music21/search/base.py | 14 +-- music21/search/lyrics.py | 8 +- music21/sieve.py | 4 +- music21/stream/base.py | 222 +++++++++++++++++++++------------- music21/stream/core.py | 17 +-- music21/stream/iterator.py | 27 +++-- music21/style.py | 6 +- music21/tablature.py | 2 +- music21/tree/spans.py | 39 +++--- music21/tree/verticality.py | 3 + requirements_dev.txt | 2 +- 16 files changed, 263 insertions(+), 174 deletions(-) diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index ea62caa61f..c7f7359be5 100644 --- a/music21/chord/__init__.py +++ b/music21/chord/__init__.py @@ -705,12 +705,11 @@ class Chord(ChordBase): >>> riteOfSpring - Incorrect entries raise a ChordException: + Incorrect entries raise a TypeError: >>> chord.Chord([base]) Traceback (most recent call last): - music21.chord.ChordException: Could not process input - argument + TypeError: Could not process input argument **Equality** @@ -1190,7 +1189,7 @@ def annotateIntervals( This chord was giving us problems: >>> c4 = chord.Chord(['G4', 'E4', 'B3', 'E3']) - >>> c4.annotateIntervals(stripSpecifiers=False) + >>> c4.annotateIntervals(inPlace=True, stripSpecifiers=False) >>> [ly.text for ly in c4.lyrics] ['m3', 'P8', 'P5'] >>> c4.annotateIntervals(inPlace=True, stripSpecifiers=False, returnList=True) diff --git a/music21/common/numberTools.py b/music21/common/numberTools.py index 6831731f73..e5ff33f883 100644 --- a/music21/common/numberTools.py +++ b/music21/common/numberTools.py @@ -735,7 +735,7 @@ def weightedSelection(values: list[int], return values[index] -def approximateGCD(values: Collection[int | float], grain: float = 1e-4) -> float: +def approximateGCD(values: Collection[int | float | Fraction], grain: float = 1e-4) -> float: ''' Given a list of values, find the lowest common divisor of floating point values. diff --git a/music21/metadata/__init__.py b/music21/metadata/__init__.py index 47ebed5efa..e9f090156e 100755 --- a/music21/metadata/__init__.py +++ b/music21/metadata/__init__.py @@ -145,6 +145,7 @@ import pathlib import re import typing as t +from typing import overload import unittest from music21 import base @@ -853,7 +854,21 @@ def __setattr__(self, name: str, value: t.Any): # bare attributes (including the ones in base classes). super().__setattr__(name, value) - def __getitem__(self, key: str) -> tuple[ValueType, ...]: + + @overload + def __getitem__(self, + key: t.Literal[ + 'movementName', + 'movementNumber', + 'title', + ]) -> tuple[Text, ...]: + pass + + @overload + def __getitem__(self, key: str) -> tuple[Text, ...]: + pass + + def __getitem__(self, key: str) -> tuple[ValueType, ...] | tuple[Text, ...]: ''' "Dictionary key" access for all standard uniqueNames and standard keys of the form 'namespace:name'. diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 14ba456115..92c27c7705 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -2372,7 +2372,10 @@ def setIdentification(self): return mxId - def metadataToMiscellaneous(self, md=None): + def metadataToMiscellaneous( + self, + md: metadata.Metadata | None = None + ) -> Element | None: # noinspection PyShadowingNames ''' Returns an mxMiscellaneous of information from metadata object md or @@ -2400,8 +2403,7 @@ def metadataToMiscellaneous(self, md=None): mxMiscellaneous = Element('miscellaneous') foundOne = False - allItems: list[tuple[str, t.Any]] = [] - + allItems: tuple[tuple[str, t.Any], ...] allItems = md.all( skipContributors=True, # we don't want the contributors (already handled them) returnPrimitives=True, # we want ValueType values @@ -2553,7 +2555,7 @@ def getSupport(attribute, supports_type, value, element): return supportsList - def setTitles(self): + def setTitles(self) -> None: ''' puts work (with work-title), movement-number, movement-title into the self.xmlRoot ''' @@ -3148,7 +3150,7 @@ def __init__(self, self.mxTranspose = None self.measureOffsetStart = 0.0 self.offsetInMeasure = 0.0 - self.currentVoiceId: int | None = None + self.currentVoiceId: int | str | None = None self.nextFreeVoiceNumber: int = 1 self.nextArpeggioNumber: int = 1 self.arpeggioNumbers: dict[expressions.ArpeggioMarkSpanner, int] = {} @@ -3207,7 +3209,12 @@ def mainElementsParse(self): # Assumes voices are flat... self.parseFlatElements(v, backupAfterwards=backupAfterwards) - def parseFlatElements(self, m, *, backupAfterwards=False): + def parseFlatElements( + self, + m: stream.Measure | stream.Voice, + *, + backupAfterwards: bool = False + ) -> None: ''' Deals with parsing all the elements in .elements, assuming that .elements is flat. @@ -3222,8 +3229,8 @@ def parseFlatElements(self, m, *, backupAfterwards=False): root = self.xmlRoot divisions = self.currentDivisions self.offsetInMeasure = 0.0 + voiceId: int | str | None if isinstance(m, stream.Voice): - m: stream.Voice if isinstance(m.id, int) and m.id < defaults.minIdNumberToConsiderMemoryLocation: voiceId = m.id self.nextFreeVoiceNumber = voiceId + 1 @@ -3242,7 +3249,9 @@ def parseFlatElements(self, m, *, backupAfterwards=False): # group all objects by offsets and then do a different order than normal sort. # that way chord symbols and other 0-width objects appear before notes as much as # possible. - for objGroup in OffsetIterator(m): + objGroup: list[base.Music21Object] + objIterator: OffsetIterator[base.Music21Object] = OffsetIterator(m) + for objGroup in objIterator: groupOffset = m.elementOffset(objGroup[0]) amountToMoveForward = int(round(divisions * (groupOffset - self.offsetInMeasure))) diff --git a/music21/repeat.py b/music21/repeat.py index 1efa2ccd13..ac8a20c55f 100644 --- a/music21/repeat.py +++ b/music21/repeat.py @@ -22,6 +22,7 @@ import string import typing as t +from music21.common.types import StreamType from music21 import environment from music21 import exceptions21 from music21 import expressions @@ -649,7 +650,7 @@ class ExpanderException(exceptions21.Music21Exception): pass -class Expander: +class Expander(t.Generic[StreamType]): ''' The Expander object can expand a single Part or Part-like Stream with repeats. Nested repeats given with :class:`~music21.bar.Repeat` objects, or @@ -720,37 +721,38 @@ class Expander: Test empty expander: >>> e = repeat.Expander() - ''' - def __init__(self, streamObj=None): - self._src = streamObj - self._repeatBrackets = None - if streamObj is not None: - self._setup() - def _setup(self): - ''' - run several setup routines. - ''' + THIS IS IN OMIT + ''' + def __init__(self, streamObj: StreamType): from music21 import stream + self._src: StreamType = streamObj + # get and store the source measure count; this is presumed to # be a Stream with Measures - self._srcMeasureStream = self._src.getElementsByClass(stream.Measure).stream() + self._srcMeasureStream: stream.Stream[stream.Measure] = self._src.getElementsByClass( + stream.Measure + ).stream() # store all top-level non Measure elements for later insertion - self._srcNotMeasureStream = self._src.getElementsNotOfClass(stream.Measure).stream() + self._srcNotMeasureStream: stream.Stream = self._src.getElementsNotOfClass( + stream.Measure + ).stream() # see if there are any repeat brackets - self._repeatBrackets = self._src.flatten().getElementsByClass( - spanner.RepeatBracket - ).stream() + self._repeatBrackets: stream.Stream[spanner.RepeatBracket] = ( + self._src.flatten().getElementsByClass(spanner.RepeatBracket).stream() + ) - self._srcMeasureCount = len(self._srcMeasureStream) + self._srcMeasureCount: int = len(self._srcMeasureStream) if self._srcMeasureCount == 0: raise ExpanderException('no measures found in the source stream to be expanded') # store counts of all non barline elements. # doing class matching by string as problems matching in some test cases - reStream = self._srcMeasureStream.flatten().getElementsByClass(RepeatExpression).stream() + reStream: stream.Stream[RepeatExpression] = ( + self._srcMeasureStream.flatten().getElementsByClass(RepeatExpression).stream() + ) self._codaCount = len(reStream.getElementsByClass(Coda)) self._segnoCount = len(reStream.getElementsByClass(Segno)) self._fineCount = len(reStream.getElementsByClass(Fine)) @@ -764,7 +766,7 @@ def _setup(self): self._dsafCount = len(reStream.getElementsByClass(DalSegnoAlFine)) self._dsacCount = len(reStream.getElementsByClass(DalSegnoAlCoda)) - def process(self, deepcopy=True): + def process(self, deepcopy: bool = True) -> StreamType: ''' This is the main call for Expander @@ -789,7 +791,7 @@ def process(self, deepcopy=True): srcStream = self._srcMeasureStream if canExpand is None: - return srcStream + return t.cast(StreamType, srcStream) # these must be copied, otherwise we have the original still self._repeatBrackets = copy.deepcopy(self._repeatBrackets) diff --git a/music21/search/base.py b/music21/search/base.py index d1a1bc0d5b..565ef352a8 100644 --- a/music21/search/base.py +++ b/music21/search/base.py @@ -194,7 +194,7 @@ class StreamSearcher: why doesn't this work? thisStream[found].expressions.append(expressions.TextExpression('*')) ''' - def __init__(self, streamSearch=None, searchList=None): + def __init__(self, streamSearch: Stream, searchList: list[m21Base.Music21Object]): self.streamSearch = streamSearch self.searchList = searchList self.recurse = False @@ -202,7 +202,7 @@ def __init__(self, streamSearch=None, searchList=None): self.filterNotesAndRests = False self.algorithms: list[ - Callable[[Stream, m21Base.Music21Object], + Callable[[StreamSearcher, Stream, m21Base.Music21Object], bool | None] ] = [StreamSearcher.wildcardAlgorithm] @@ -259,23 +259,23 @@ def run(self): return foundEls - def wildcardAlgorithm(self, streamEl, searchEl): + def wildcardAlgorithm(self, streamEl: Stream, searchEl: m21Base.Music21Object): ''' An algorithm that supports Wildcards -- added by default to the search function. ''' - if Wildcard in searchEl.classSet: + if isinstance(searchEl, Wildcard): return True else: return None - def rhythmAlgorithm(self, streamEl, searchEl): - if 'WildcardDuration' in searchEl.duration.classes: + def rhythmAlgorithm(self, streamEl: Stream, searchEl: m21Base.Music21Object): + if isinstance(searchEl.duration, WildcardDuration): return True if searchEl.duration.quarterLength != streamEl.duration.quarterLength: return False return None - def noteNameAlgorithm(self, streamEl, searchEl): + def noteNameAlgorithm(self, streamEl: Stream, searchEl: m21Base.Music21Object): if not hasattr(searchEl, 'name'): return False if not hasattr(streamEl, 'name'): diff --git a/music21/search/lyrics.py b/music21/search/lyrics.py index a1ab465088..d15ee81b70 100644 --- a/music21/search/lyrics.py +++ b/music21/search/lyrics.py @@ -16,12 +16,16 @@ from collections import namedtuple, OrderedDict import re import typing as t +from typing import TYPE_CHECKING import unittest from music21.exceptions21 import Music21Exception from music21 import note # from music21 import common +if TYPE_CHECKING: + from music21.common.types import StreamType + LINEBREAK_TOKEN = ' // ' _attrList = 'el start end measure lyric text identifier absoluteStart absoluteEnd'.split() @@ -132,8 +136,8 @@ class LyricSearcher: found if a work contains multiple voices. ''' - def __init__(self, s=None): - self.stream = s + def __init__(self, s: StreamType | None = None): + self.stream: StreamType | None = s self.includeIntermediateElements = False # currently does nothing self.includeTrailingMelisma = False # currently does nothing diff --git a/music21/sieve.py b/music21/sieve.py index c046628ecd..a392f38b93 100644 --- a/music21/sieve.py +++ b/music21/sieve.py @@ -1010,7 +1010,7 @@ class Sieve: >>> c = sieve.Sieve('(5|2)&4&8') ''' - def __init__(self, usrStr, z=None): + def __init__(self, usrStr: str | list[str], z: list[int] | None = None): # note: this z should only be used if usrStr is a str, and not a list if z is None and isinstance(usrStr, str): z = list(range(100)) @@ -1025,7 +1025,7 @@ def __init__(self, usrStr, z=None): self._nonCompressible = False # if current z provides a nullSeg; no compression # variables will re-initialize w/ dedicated methods - self._resLib = {} # store id and object + self._resLib: dict[int, Residual] = {} # store id and object self._resId = 0 # used to calculate residual ids # expanded, compressed form diff --git a/music21/stream/base.py b/music21/stream/base.py index acafb3ce9a..e29f1d2929 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -81,6 +81,7 @@ T = t.TypeVar('T') # we sometimes need to return a different type. ChangedM21ObjType = t.TypeVar('ChangedM21ObjType', bound=base.Music21Object) +RecursiveLyricList = note.Lyric | None | list['RecursiveLyricList'] BestQuantizationMatch = namedtuple( 'BestQuantizationMatch', @@ -4918,7 +4919,7 @@ def optionalAddRest(): def measureOffsetMap( self, classFilterList: list[t.Type] | list[str] | tuple[t.Type] | tuple[str] = ('Measure',) - ): + ) -> OrderedDict[float | Fraction, list[Measure]]: ''' If this Stream contains Measures, returns an OrderedDict whose keys are the offsets of the start of each measure @@ -5203,10 +5204,33 @@ def atSoundingPitch(self, value: bool | t.Literal['unknown']): else: raise StreamException(f'not a valid at sounding pitch value: {value}') - def _transposeByInstrument(self, - reverse=False, - inPlace=False, - transposeKeySignature=True): + @overload + def _transposeByInstrument( + self: StreamType, + *, + reverse: bool = False, + transposeKeySignature: bool = True, + inPlace: t.Literal[True], + ) -> None: + pass + + @overload + def _transposeByInstrument( + self: StreamType, + *, + reverse: bool = False, + transposeKeySignature: bool = True, + inPlace: t.Literal[False] = False, + ) -> StreamType: + pass + + def _transposeByInstrument( + self: StreamType, + *, + reverse: bool = False, + transposeKeySignature: bool = True, + inPlace: bool = False, + ) -> StreamType | None: ''' Transpose the Stream according to each instrument's transposition. @@ -5235,6 +5259,7 @@ def _transposeByInstrument(self, instrument_stream = instrument_stream.extendDuration('Instrument', inPlace=False) # store class filter list for transposition + classFilterList: tuple[type[music21.Music21Object], ...] if transposeKeySignature: classFilterList = (note.Note, chord.Chord, key.KeySignature) else: @@ -5245,7 +5270,7 @@ def _transposeByInstrument(self, continue start = inst.offset end = start + inst.quarterLength - focus = returnObj.flatten().getElementsByOffset( + focus: Stream = returnObj.flatten().getElementsByOffset( start, end, includeEndBoundary=False, @@ -6279,7 +6304,7 @@ def chordifyOneMeasure(templateInner, streamToChordify): ''' streamToChordify is either a Measure or a Score=MeasureSlice ''' - timespanTree = streamToChordify.asTimespans(classList=('GeneralNote',)) + timespanTree = streamToChordify.asTimespans(classList=(note.GeneralNote,)) allTimePoints = timespanTree.allTimePoints() if 0 not in allTimePoints: allTimePoints = (0,) + allTimePoints @@ -6341,10 +6366,9 @@ def removeConsecutiveRests(templateInner, consecutiveRests): else: workObj = self - templateStream: Stream if self.hasPartLikeStreams(): # use the measure boundaries of the first Part as a template. - templateStream = workObj.getElementsByClass('Stream').first() + templateStream = workObj.getElementsByClass(Stream).first() else: templateStream = workObj @@ -6354,10 +6378,8 @@ def removeConsecutiveRests(templateInner, consecutiveRests): if template.hasMeasures(): measureIterator = template.getElementsByClass(Measure) - templateMeasure: 'Measure' for i, templateMeasure in enumerate(measureIterator): # measurePart is likely a Score (MeasureSlice), not a measure - measurePart: 'Measure' measurePart = workObj.measure(i, collect=(), indicesNotNumbers=True) if measurePart is not None: chordifyOneMeasure(templateMeasure, measurePart) @@ -7146,8 +7168,7 @@ def stripTies( # part-like does not necessarily mean that the next level down is a stream.Part # object or that this is a stream.Score object, so do not substitute # returnObj.parts for this... - p: Part - for p in returnObj.getElementsByClass('Stream'): + for p in returnObj.getElementsByClass(Stream): # already copied if necessary; edit in place p.stripTies(inPlace=True, matchByPitch=matchByPitch) if not inPlace: @@ -9352,7 +9373,7 @@ def bestMatch(target, divisors): if inPlace is False: return returnStream - def expandRepeats(self, copySpanners=True): + def expandRepeats(self: StreamType, copySpanners: bool = True) -> StreamType: ''' Expand this Stream with repeats. Nested repeats given with :class:`~music21.bar.Repeat` objects, or @@ -9363,9 +9384,8 @@ def expandRepeats(self, copySpanners=True): deepcopies of all contained elements at all levels. Uses the :class:`~music21.repeat.Expander` object in the `repeat` module. - - TODO: DOC TEST ''' + # TODO: needs DOC TEST if not self.hasMeasures(): raise StreamException( 'cannot process repeats on Stream that does not contain measures' @@ -9376,7 +9396,7 @@ def expandRepeats(self, copySpanners=True): # copy all non-repeats # do not copy repeat brackets - for e in self.getElementsNotOfClass('Measure'): + for e in self.getElementsNotOfClass(Measure): if 'RepeatBracket' not in e.classes: eNew = copy.deepcopy(e) # assume that this is needed post.insert(self.elementOffset(e), eNew) @@ -9423,16 +9443,14 @@ def sliceByQuarterLengths(self, quarterLengthList, *, target=None, if returnObj.hasMeasures(): # call on component measures - m: Measure - for m in returnObj.getElementsByClass('Measure'): + for m in returnObj.getElementsByClass(Measure): m.sliceByQuarterLengths(quarterLengthList, target=target, addTies=addTies, inPlace=True) returnObj.coreElementsChanged() return returnObj # exit if returnObj.hasPartLikeStreams(): - p: Part - for p in returnObj.getElementsByClass('Part'): + for p in returnObj.getElementsByClass(Part): p.sliceByQuarterLengths(quarterLengthList, target=target, addTies=addTies, inPlace=True) returnObj.coreElementsChanged() @@ -9563,8 +9581,7 @@ def sliceAtOffsets( if returnObj.hasPartLikeStreams(): # part-like requires getting Streams, not Parts - p: Stream - for p in returnObj.getElementsByClass('Stream'): + for p in returnObj.getElementsByClass(Stream): offsetListLocal = [o - p.getOffsetBySite(returnObj) for o in offsetList] p.sliceAtOffsets(offsetList=offsetListLocal, addTies=addTies, @@ -9632,8 +9649,7 @@ def sliceByBeat(self, if returnObj.hasMeasures(): # call on component measures - m: Measure - for m in returnObj.getElementsByClass('Measure'): + for m in returnObj.getElementsByClass(Measure): m.sliceByBeat(target=target, addTies=addTies, inPlace=True, @@ -9641,8 +9657,7 @@ def sliceByBeat(self, return returnObj # exit if returnObj.hasPartLikeStreams(): - p: Part - for p in returnObj.getElementsByClass('Part'): + for p in returnObj.getElementsByClass(Part): p.sliceByBeat(target=target, addTies=addTies, inPlace=True, @@ -9774,14 +9789,13 @@ def hasPartLikeStreams(self): multiPart = False if not self.isFlat: # if flat, does not have parts! # do not need to look in endElements - obj: Stream - for obj in self.getElementsByClass('Stream'): + for obj in self.getElementsByClass(Stream): # if obj is a Part, we have multi-parts - if 'Part' in obj.classes: + if isinstance(obj, Part): multiPart = True break - elif 'Measure' in obj.classes or 'Voice' in obj.classes: + elif isinstance(obj, (Measure, Voice)): multiPart = False break @@ -10382,7 +10396,7 @@ def _durSpanOverlap(self, a, b, includeEndBoundary=False): found = True return found - def _findLayering(self): + def _findLayering(self) -> list[list[int]]: ''' Find any elements in an elementsSorted list that have durations that cause overlaps. @@ -10409,7 +10423,7 @@ def _findLayering(self): # create a list with an entry for each element # in each entry, provide indices of all other elements that overlap - overlapMap = [[] for dummy in range(len(durSpanSorted))] + overlapMap: list[list[int]] = [[] for dummy in range(len(durSpanSorted))] for i in range(len(durSpanSortedIndex)): src = durSpanSortedIndex[i] @@ -11260,8 +11274,7 @@ def voicesToParts(self, *, separateById=False): # add all parts to one Score if self.hasPartLikeStreams(): # part-like does not necessarily mean .parts - p: Stream - for p in self.getElementsByClass('Stream'): + for p in self.getElementsByClass(Stream): sSub = p.voicesToParts(separateById=separateById) for pSub in sSub: s.insert(0, pSub) @@ -11463,7 +11476,12 @@ def flattenUnnecessaryVoices(self, *, force=False, inPlace=False): # Lyric control # might be overwritten in base.splitAtDurations, but covered with a check # pylint: disable=method-hidden - def lyrics(self, ignoreBarlines=True, recurse=False, skipTies=False): + def lyrics( + self, + ignoreBarlines: bool = True, + recurse: bool = False, + skipTies: bool = False, + ) -> dict[int, list[RecursiveLyricList]]: # noinspection PyShadowingNames ''' Returns a dict of lists of lyric objects (with the keys being @@ -11528,33 +11546,36 @@ def lyrics(self, ignoreBarlines=True, recurse=False, skipTies=False): [] ''' - returnLists = {} + returnLists: dict[int, list[RecursiveLyricList]] = {} numNotes = 0 # -------------------- # noinspection PyShadowingNames - def appendLyricsFromNote(n, returnLists, numNonesToAppend): + def appendLyricsFromNote( + n: note.NotRest, + inner_returnLists: dict[int, list[RecursiveLyricList]], + numNonesToAppend: int + ): if not n.lyrics: - for k in returnLists: - returnLists[k].append(None) + for k in inner_returnLists: + inner_returnLists[k].append(None) return addLyricNums = [] for ly in n.lyrics: - if ly.number not in returnLists: - returnLists[ly.number] = [None for dummy in range(numNonesToAppend)] - returnLists[ly.number].append(ly) + if ly.number not in inner_returnLists: + inner_returnLists[ly.number] = [None for dummy in range(numNonesToAppend)] + inner_returnLists[ly.number].append(ly) addLyricNums.append(ly.number) - for k in returnLists: + for k in inner_returnLists: if k not in addLyricNums: - returnLists[k].append(None) + inner_returnLists[k].append(None) # ----------------------- # TODO: use new recurse for e in self: - eClasses = e.classes - if ignoreBarlines is True and 'Measure' in eClasses: + if ignoreBarlines is True and isinstance(e, Measure): m = e for n in m.notes: if skipTies is True: @@ -11567,24 +11588,22 @@ def appendLyricsFromNote(n, returnLists, numNonesToAppend): appendLyricsFromNote(n, returnLists, numNotes) numNotes += 1 - elif recurse is True and 'Stream' in eClasses: - s = e - sublists = s.lyrics(ignoreBarlines=ignoreBarlines, recurse=True, skipTies=skipTies) + elif recurse is True and isinstance(e, Stream): + sublists = e.lyrics(ignoreBarlines=ignoreBarlines, recurse=True, skipTies=skipTies) for k in sublists: if k not in returnLists: returnLists[k] = [] returnLists[k].append(sublists[k]) - elif 'NotRest' in eClasses: # elif 'Stream' not in eClasses and hasattr(e, 'lyrics'): + elif isinstance(e, note.NotRest): # elif 'Stream' not in eClasses and hasattr(e, 'lyrics'): # noinspection PyTypeChecker - n: 'music21.note.NotRest' = e if skipTies is True: - if n.tie is None or n.tie.type == 'start': - appendLyricsFromNote(n, returnLists, numNotes) + if e.tie is None or e.tie.type == 'start': + appendLyricsFromNote(e, returnLists, numNotes) numNotes += 1 else: pass # do nothing if end tie and skipTies is True else: - appendLyricsFromNote(n, returnLists, numNotes) + appendLyricsFromNote(e, returnLists, numNotes) numNotes += 1 else: # e is a stream @@ -13788,8 +13807,7 @@ def measure(self, # this calls on Music21Object, transfers id, groups post.mergeAttributes(self) # note that this will strip all objects that are not Parts - p: Part - for p in self.getElementsByClass('Part'): + for p in self.getElementsByClass(Part): # insert all at zero mStream = p.measures(startMeasureNumber, endMeasureNumber, @@ -13809,25 +13827,26 @@ def measure(self, return post - def expandRepeats(self): + def expandRepeats(self: Score, copySpanners: bool = True) -> Score: ''' Expand all repeats, as well as all repeat indications given by text expressions such as D.C. al Segno. This method always returns a new Stream, with deepcopies of all contained elements at all level. + + Note that copySpanners is ignored here, as they are always copied. ''' post = self.cloneEmpty(derivationMethod='expandRepeats') # this calls on Music21Object, transfers id, groups post.mergeAttributes(self) # get all things in the score that are not Parts - for e in self.iter().getElementsNotOfClass('Part'): + for e in self.iter().getElementsNotOfClass(Part): eNew = copy.deepcopy(e) # assume that this is needed post.insert(self.elementOffset(e), eNew) - p: Part - for p in self.getElementsByClass('Part'): + for p in self.getElementsByClass(Part): # get spanners at highest level, not by Part post.insert(0, p.expandRepeats(copySpanners=False)) @@ -13843,7 +13862,10 @@ def expandRepeats(self): spannerBundle.replaceSpannedElement(origin, e) return post - def measureOffsetMap(self, classFilterList=None): + def measureOffsetMap( + self, + classFilterList: list[t.Type] | list[str] | tuple[t.Type] | tuple[str] = ('Measure',) + ) -> OrderedDict[float | Fraction, list[Measure]]: ''' This Score method overrides the :meth:`~music21.stream.Stream.measureOffsetMap` method of Stream. @@ -13861,7 +13883,7 @@ def measureOffsetMap(self, classFilterList=None): if not parts: return Stream.measureOffsetMap(self, classFilterList) # else: - offsetMap = {} + offsetMap: dict[float | Fraction, list[Measure]] = {} for p in parts: mapPartial = p.measureOffsetMap(classFilterList) # environLocal.printDebug(['mapPartial', mapPartial]) @@ -13874,7 +13896,31 @@ def measureOffsetMap(self, classFilterList=None): orderedOffsetMap = OrderedDict(sorted(offsetMap.items(), key=lambda o: o[0])) return orderedOffsetMap - def sliceByGreatestDivisor(self, *, addTies=True, inPlace=False): + + @overload + def sliceByGreatestDivisor( + self: Score, + *, + addTies: bool = True, + inPlace: t.Literal[True], + ) -> None: + pass + + @overload + def sliceByGreatestDivisor( + self: Score, + *, + addTies: bool = True, + inPlace: t.Literal[False] = False, + ) -> Score: + pass + + def sliceByGreatestDivisor( + self: Score, + *, + addTies: bool = True, + inPlace: bool = False, + ) -> Score | None: ''' Slice all duration of all part by the minimum duration that can be summed to each concurrent duration. @@ -13889,21 +13935,26 @@ def sliceByGreatestDivisor(self, *, addTies=True, inPlace=False): # Find the greatest divisor for each measure at a time. # If there are no measures this will be zero. - mStream = returnObj.parts.first().getElementsByClass(Measure) + firstPart = returnObj.parts.first() + if firstPart is None: + raise TypeError('Cannot sliceByGreatestDivisor without parts') + mStream = firstPart.getElementsByClass(Measure) mCount = len(mStream) if mCount == 0: mCount = 1 # treat as a single measure + + m_or_p: Measure | Part for i in range(mCount): # may be 1 uniqueQuarterLengths = [] p: Part - for p in returnObj.getElementsByClass('Part'): + for p in returnObj.getElementsByClass(Part): if p.hasMeasures(): - m = p.getElementsByClass(Measure)[i] + m_or_p = p.getElementsByClass(Measure)[i] else: - m = p # treat the entire part as one measure + m_or_p = p # treat the entire part as one measure # collect all unique quarter lengths - for e in m.notesAndRests: + for e in m_or_p.notesAndRests: # environLocal.printDebug(['examining e', i, e, e.quarterLength]) if e.quarterLength not in uniqueQuarterLengths: uniqueQuarterLengths.append(e.quarterLength) @@ -13913,18 +13964,17 @@ def sliceByGreatestDivisor(self, *, addTies=True, inPlace=False): # environLocal.printDebug(['Score.sliceByGreatestDivisor: # got divisor from unique ql:', divisor, uniqueQuarterLengths]) - p: Part - for p in returnObj.getElementsByClass('Part'): + for p in returnObj.getElementsByClass(Part): # in place: already have a copy if nec # must do on measure at a time if p.hasMeasures(): - m = p.getElementsByClass(Measure)[i] + m_or_p = p.getElementsByClass(Measure)[i] else: - m = p # treat the entire part as one measure - m.sliceByQuarterLengths(quarterLengthList=[divisor], - target=None, - addTies=addTies, - inPlace=True) + m_or_p = p # treat the entire part as one measure + m_or_p.sliceByQuarterLengths(quarterLengthList=[divisor], + target=None, + addTies=addTies, + inPlace=True) del mStream # cleanup Streams returnObj.coreElementsChanged() if not inPlace: @@ -14071,12 +14121,14 @@ def implode(self): permitOneVoicePerPart=permitOneVoicePerPart ) - def makeNotation(self, - meterStream=None, - refStreamOrTimeRange=None, - inPlace=False, - bestClef=False, - **subroutineKeywords): + def makeNotation( + self, + meterStream=None, + refStreamOrTimeRange=None, + inPlace=False, + bestClef=False, + **subroutineKeywords + ): ''' This method overrides the makeNotation method on Stream, such that a Score object with one or more Parts or Streams @@ -14086,7 +14138,7 @@ def makeNotation(self, If `inPlace` is True, this is done in-place; if `inPlace` is False, this returns a modified deep copy. ''' - returnStream: Score + # returnStream: Score if inPlace: returnStream = self else: @@ -14095,7 +14147,7 @@ def makeNotation(self, # do not assume that we have parts here if self.hasPartLikeStreams(): - s: Stream + # s: Stream for s in returnStream.getElementsByClass('Stream'): # process all component Streams inPlace s.makeNotation(meterStream=meterStream, diff --git a/music21/stream/core.py b/music21/stream/core.py index 64b08ccd63..e50e57422d 100644 --- a/music21/stream/core.py +++ b/music21/stream/core.py @@ -47,7 +47,7 @@ class StreamCore(Music21Object): Core aspects of a Stream's behavior. Any of these can change at any time. Users are encouraged only to create stream.Stream objects. ''' - def __init__(self, **keywords): + def __init__(self, **keywords) -> None: super().__init__(**keywords) # hugely important -- keeps track of where the _elements are # the _offsetDict is a dictionary where id(element) is the @@ -207,11 +207,11 @@ def coreSetElementOffset( def coreElementsChanged( self, *, - updateIsFlat=True, - clearIsSorted=True, - memo=None, - keepIndex=False, - ): + updateIsFlat: bool = True, + clearIsSorted: bool = True, + memo: list[int] | None = None, + keepIndex: bool = False, + ) -> None: ''' NB -- a "core" stream method that is not necessary for most users. @@ -239,7 +239,7 @@ def coreElementsChanged( False ''' # experimental - if not self._mutable: + if not getattr(self, '_mutable', True): raise ImmutableStreamException( 'coreElementsChanged should not be triggered on an immutable stream' ) @@ -263,7 +263,8 @@ def coreElementsChanged( if self._derivation is not None: sdm = self._derivation.method if sdm in ('flat', 'semiflat'): - origin: Stream = self._derivation.origin + origin: 'music21.base.Stream' = t.cast('music21.base.Stream', + self._derivation.origin) origin.clearCache() # may not always need to clear cache of all living sites, but may diff --git a/music21/stream/iterator.py b/music21/stream/iterator.py index eadc13037f..3efd317b36 100644 --- a/music21/stream/iterator.py +++ b/music21/stream/iterator.py @@ -1577,7 +1577,7 @@ def __init__(self, restoreActiveSites=True, activeInformation=None, ignoreSorting=False - ): + ) -> None: super().__init__(srcStream, # restrictClass=restrictClass, filterList=filterList, @@ -1593,7 +1593,7 @@ def __next__(self) -> list[M21ObjType]: # type: ignore if self.raiseStopIterationNext: raise StopIteration - retElementList = None + retElementList: list[M21ObjType] = [] # make sure that cleanup is not called during the loop... try: if self.nextToYield: @@ -1761,17 +1761,18 @@ class RecursiveIterator(StreamIterator, Sequence[M21ObjType]): >>> bool(expressive) True ''' - def __init__(self, - srcStream, - *, - # restrictClass: type[M21ObjType] = base.Music21Object, - filterList=None, - restoreActiveSites=True, - activeInformation=None, - streamsOnly=False, - includeSelf=False, - ignoreSorting=False - ): # , parentIterator=None): + def __init__( + self, + srcStream, + *, + # restrictClass: type[M21ObjType] = base.Music21Object, + filterList=None, + restoreActiveSites=True, + activeInformation=None, + streamsOnly=False, + includeSelf=False, + ignoreSorting=False + ) -> None: # , parentIterator=None): super().__init__(srcStream, # restrictClass=restrictClass, filterList=filterList, diff --git a/music21/style.py b/music21/style.py index bea7ac3945..668cb92a03 100644 --- a/music21/style.py +++ b/music21/style.py @@ -78,7 +78,7 @@ class Style(ProtoM21Object): ''', } - def __init__(self): + def __init__(self) -> None: self.size = None self.relativeX: float | int | None = None @@ -267,7 +267,7 @@ class NoteStyle(Style): ''', } - def __init__(self): + def __init__(self) -> None: super().__init__() self.stemStyle: Style | None = None self.accidentalStyle: Style | None = None @@ -622,7 +622,7 @@ class StyleMixin(common.SlottedObjectMixin): __slots__ = ('_style', '_editorial') - def __init__(self): + def __init__(self) -> None: # no need to call super().__init__() on SlottedObjectMixin # This might be dangerous though self._style: Style | None = None diff --git a/music21/tablature.py b/music21/tablature.py index f1ac8fe2de..63acf7e860 100644 --- a/music21/tablature.py +++ b/music21/tablature.py @@ -202,7 +202,7 @@ def getFretNoteByString(self, requestedString): return None - def getPitches(self): + def getPitches(self) -> list[None | pitch.Pitch]: ''' Returns a list of all the pitches (or None for each) given the FretNote information. This requires a tuning to be set. diff --git a/music21/tree/spans.py b/music21/tree/spans.py index ed8d29807b..cae6775ac4 100644 --- a/music21/tree/spans.py +++ b/music21/tree/spans.py @@ -19,11 +19,16 @@ import copy from math import inf +from typing import TYPE_CHECKING import unittest +from music21.common.types import OffsetQLIn from music21 import environment from music21 import exceptions21 +if TYPE_CHECKING: + from music21 import base + from music21 import stream environLocal = environment.Environment('tree.spans') # ----------------------------------------------------------------------------- @@ -318,19 +323,18 @@ class ElementTimespan(Timespan): # INITIALIZER # - def __init__(self, - element=None, - parentOffset=None, - parentEndTime=None, - parentage=None, - offset=None, - endTime=None, - ): + def __init__( + self, + element: base.Music21Object | None = None, + parentOffset: OffsetQLIn | None = None, + parentEndTime: OffsetQLIn | None = None, + parentage: tuple[stream.Stream, ...] = (), + offset: OffsetQLIn | None = None, + endTime: OffsetQLIn | None = None, + ): super().__init__(offset=offset, endTime=endTime) - self.element = element - if parentage is not None: - parentage = tuple(parentage) + self.element: base.Music21Object | None = element self.parentage = parentage if parentOffset is not None: parentOffset = float(parentOffset) @@ -494,7 +498,7 @@ def part(self): from music21 import stream return self.getParentageByClass(classList=(stream.Part,)) - def makeElement(self, makeCopy=True): + def makeElement(self, makeCopy: bool = True) -> base.Music21Object | None: ''' Return a copy of the element (or the same one if makeCopy is False) with the quarterLength set to the length of the timespan @@ -504,7 +508,6 @@ def makeElement(self, makeCopy=True): return None if makeCopy: - el: 'music21.base.Music21Object' el_old = el el = copy.deepcopy(el_old) el.derivation.origin = el_old @@ -526,11 +529,11 @@ def __init__(self, endTime=None, ): super().__init__(element=element, - parentOffset=parentOffset, - parentEndTime=parentEndTime, - parentage=parentage, - offset=offset, - endTime=endTime) + parentOffset=parentOffset, + parentEndTime=parentEndTime, + parentage=parentage, + offset=offset, + endTime=endTime) @property def pitches(self): diff --git a/music21/tree/verticality.py b/music21/tree/verticality.py index 424aecfb57..493ab527fa 100644 --- a/music21/tree/verticality.py +++ b/music21/tree/verticality.py @@ -19,6 +19,7 @@ from collections.abc import Iterable, Sequence import copy import itertools +from typing import TYPE_CHECKING import unittest from music21 import chord @@ -886,6 +887,8 @@ def conditionalAdd(ts, n: note.Note) -> None: for subEl in list(el)[1:]: conditionalAdd(timeSpan, subEl) else: + if TYPE_CHECKING: + assert isinstance(el, note.Note) conditionalAdd(timeSpan, el) seenArticulations = set() diff --git a/requirements_dev.txt b/requirements_dev.txt index 68aef77485..e276617574 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -3,7 +3,7 @@ pylint>=2.15.4 flake8<6.0.0 flake8-quotes hatchling -mypy +mypy>=0.990 coveralls scipy sphinx From abeac8863385d170b9c68beb48b6a6257a1ec19d Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Mon, 5 Dec 2022 21:36:39 -1000 Subject: [PATCH 04/16] fix bugs --- music21/repeat.py | 9 +++------ music21/stream/base.py | 3 +-- music21/stream/core.py | 2 +- music21/stream/tests.py | 5 ++--- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/music21/repeat.py b/music21/repeat.py index ac8a20c55f..e135656651 100644 --- a/music21/repeat.py +++ b/music21/repeat.py @@ -713,15 +713,13 @@ class Expander(t.Generic[StreamType]): {0.0} {3.0} + Changed in v9: Expander must be initialized with a Stream object. + OMIT_FROM_DOCS TODO: Note bug: barline style = double for each! Clefs and TimesSignatures should only be in first one! - Test empty expander: - - >>> e = repeat.Expander() - THIS IS IN OMIT ''' def __init__(self, streamObj: StreamType): @@ -1724,11 +1722,10 @@ def getRepeatExpressionIndex(self, streamObj, target): stream of measures. This requires the provided stream to only have measures. - >>> s = converter.parse('tinynotation: 3/4 A2. C4 D E F2.') >>> s.makeMeasures(inPlace=True) >>> s.measure(3).append(repeat.Segno()) - >>> e = repeat.Expander() + >>> e = repeat.Expander(s) getRepeatExpressionIndex returns the measureIndex not measure number diff --git a/music21/stream/base.py b/music21/stream/base.py index e29f1d2929..b5551e21e7 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -11594,8 +11594,7 @@ def appendLyricsFromNote( if k not in returnLists: returnLists[k] = [] returnLists[k].append(sublists[k]) - elif isinstance(e, note.NotRest): # elif 'Stream' not in eClasses and hasattr(e, 'lyrics'): - # noinspection PyTypeChecker + elif isinstance(e, note.NotRest): if skipTies is True: if e.tie is None or e.tie.type == 'start': appendLyricsFromNote(e, returnLists, numNotes) diff --git a/music21/stream/core.py b/music21/stream/core.py index e50e57422d..d87a6c3b01 100644 --- a/music21/stream/core.py +++ b/music21/stream/core.py @@ -263,7 +263,7 @@ def coreElementsChanged( if self._derivation is not None: sdm = self._derivation.method if sdm in ('flat', 'semiflat'): - origin: 'music21.base.Stream' = t.cast('music21.base.Stream', + origin: 'music21.base.Stream' = t.cast('music21.stream.Stream', self._derivation.origin) origin.clearCache() diff --git a/music21/stream/tests.py b/music21/stream/tests.py index c0ae1947ec..088bb3dc48 100644 --- a/music21/stream/tests.py +++ b/music21/stream/tests.py @@ -1714,7 +1714,7 @@ def testMeasureOffsetMap(self): a = corpus.parse('bach/bwv324.xml') # get notes from one measure - mOffsetMap = a.parts[0].flatten().measureOffsetMap(note.Note) + mOffsetMap = a.parts[0].flatten().measureOffsetMap([note.Note]) self.assertEqual(sorted(list(mOffsetMap.keys())), [0.0, 4.0, 8.0, 12.0, 16.0, 20.0, 24.0, 34.0, 38.0]) @@ -1743,7 +1743,6 @@ def testMeasureOffsetMap(self): [0.0, 4.0, 8.0, 12.0, 16.0, 20.0, 24.0, 34.0, 38.0]) def testMeasureOffsetMapPostTie(self): - a = corpus.parse('bach/bwv4.8') # alto line syncopated/tied notes across bars # a.show() @@ -1775,7 +1774,7 @@ def testMeasureOffsetMapPostTie(self): self.assertEqual(mNo, 4) # can we get an offset Measure map by looking for measures - post = altoPostTie.measureOffsetMap(Measure) + post = altoPostTie.measureOffsetMap([Measure]) # yes, retainContainers defaults to True self.assertEqual(list(post.keys()), correctMeasureOffsetMap) From 9e3f9fbf8e09416cfb442aee929217311ef01d81 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Mon, 5 Dec 2022 21:39:48 -1000 Subject: [PATCH 05/16] missed one --- music21/musicxml/m21ToXml.py | 2 +- music21/stream/core.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index 92c27c7705..c4c555c606 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -132,7 +132,7 @@ def normalizeColor(color): return color.upper() -def getMetadataFromContext(s): +def getMetadataFromContext(s: stream.Stream) -> metadata.Metadata | None: # noinspection PyShadowingNames ''' Get metadata from site or context, so that a Part diff --git a/music21/stream/core.py b/music21/stream/core.py index d87a6c3b01..db5d06b873 100644 --- a/music21/stream/core.py +++ b/music21/stream/core.py @@ -263,8 +263,8 @@ def coreElementsChanged( if self._derivation is not None: sdm = self._derivation.method if sdm in ('flat', 'semiflat'): - origin: 'music21.base.Stream' = t.cast('music21.stream.Stream', - self._derivation.origin) + origin: 'music21.stream.Stream' = t.cast('music21.stream.Stream', + self._derivation.origin) origin.clearCache() # may not always need to clear cache of all living sites, but may From e023303ce6b6222b1618f3d1b21bb9be7946dea3 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Mon, 5 Dec 2022 22:28:30 -1000 Subject: [PATCH 06/16] more fixes --- music21/braille/segment.py | 107 ++++++++++++++------------ music21/layout.py | 9 ++- music21/metadata/__init__.py | 7 ++ music21/musicxml/m21ToXml.py | 65 ++++++++++------ music21/musicxml/partStaffExporter.py | 8 +- music21/search/lyrics.py | 3 +- music21/stream/base.py | 31 +++++--- music21/stream/core.py | 6 +- music21/stream/tests.py | 6 +- music21/tree/spans.py | 4 +- music21/tree/verticality.py | 4 +- 11 files changed, 148 insertions(+), 102 deletions(-) diff --git a/music21/braille/segment.py b/music21/braille/segment.py index a418c1352c..9d262d96e1 100644 --- a/music21/braille/segment.py +++ b/music21/braille/segment.py @@ -154,6 +154,47 @@ def setGroupingGlobals(): # ------------------------------------------------------------------------------ class BrailleElementGrouping(ProtoM21Object): + ''' + A BrailleElementGrouping mimics a list of objects which should be displayed + without a space in braille. + + >>> from music21.braille import segment + >>> bg = segment.BrailleElementGrouping() + >>> bg.append(note.Note('C4')) + >>> bg.append(note.Note('D4')) + >>> bg.append(note.Rest()) + >>> bg.append(note.Note('F4')) + >>> bg + , + , , ]> + >>> print(bg) + + + + + + These are the defaults, and they are shared across all objects... + + >>> bg.keySignature + + >>> bg.timeSignature + + + >>> bg.descendingChords + True + + >>> bg.showClefSigns + False + + >>> bg.upperFirstInNoteFingering + True + + >>> bg.withHyphen + False + + >>> bg.numRepeats + 0 + ''' _DOC_ATTR: dict[str, str] = { 'keySignature': 'The last :class:`~music21.key.KeySignature` preceding the grouping.', 'timeSignature': 'The last :class:`~music21.meter.TimeSignature` preceding the grouping.', @@ -169,47 +210,6 @@ class BrailleElementGrouping(ProtoM21Object): 'numRepeats': 'The number of times this grouping is repeated.' } def __init__(self, *listElements): - ''' - A BrailleElementGrouping mimics a list of objects which should be displayed - without a space in braille. - - >>> from music21.braille import segment - >>> bg = segment.BrailleElementGrouping() - >>> bg.append(note.Note('C4')) - >>> bg.append(note.Note('D4')) - >>> bg.append(note.Rest()) - >>> bg.append(note.Note('F4')) - >>> bg - , - , , ]> - >>> print(bg) - - - - - - These are the defaults, and they are shared across all objects... - - >>> bg.keySignature - - >>> bg.timeSignature - - - >>> bg.descendingChords - True - - >>> bg.showClefSigns - False - - >>> bg.upperFirstInNoteFingering - True - - >>> bg.withHyphen - False - - >>> bg.numRepeats - 0 - ''' self.internalList = list(*listElements) setGroupingGlobals() @@ -221,6 +221,9 @@ def __init__(self, *listElements): self.withHyphen = False self.numRepeats = 0 + def __iter__(self): + return iter(self.internalList) + def __getitem__(self, item): return self.internalList[item] @@ -235,7 +238,7 @@ def __getattr__(self, attr): raise AttributeError('internalList not defined yet') return getattr(self.internalList, attr) - def __str__(self): + def __str__(self) -> str: ''' Return a unicode braille representation of each object in the BrailleElementGrouping. @@ -360,7 +363,7 @@ def __init__(self, lineLength: int = 40): self.groupingKeysToProcess: deque[SegmentKey] = deque() self.currentGroupingKey: SegmentKey | None = None self.previousGroupingKey: SegmentKey | None = None - self.lastNote = None + self.lastNote: note.Note | None = None self.showClefSigns: bool = False self.upperFirstInNoteFingering: bool = True @@ -590,8 +593,10 @@ def extractHeading(self): self.addHeading(brailleHeading) - def extractInaccordGrouping(self): - inaccords = self._groupingDict.get(self.currentGroupingKey) + def extractInaccordGrouping(self) -> None: + if self.currentGroupingKey is None: + raise ValueError('currentGroupingKey must not be None to call extractInaccordGrouping') + inaccords = self._groupingDict[self.currentGroupingKey] last_clef: clef.Clef | None = None seen_voice: bool = False for music21VoiceOrClef in inaccords: @@ -624,7 +629,7 @@ def extractLongExpressionGrouping(self): longExprInBraille = basic.textExpressionToBraille(longTextExpression) self.addLongExpression(longExprInBraille) - def showLeadingOctaveFromNoteGrouping(self, noteGrouping): + def showLeadingOctaveFromNoteGrouping(self, noteGrouping: BrailleElementGrouping): ''' Given a noteGrouping, should we show the octave symbol? @@ -793,12 +798,14 @@ def splitNoteGroupingAndTranscribe(self, return (brailleNoteGroupingA, brailleNoteGroupingB) - def extractNoteGrouping(self): + def extractNoteGrouping(self) -> None: ''' Fundamentally important method that adds a noteGrouping to the braille line. ''' transcriber = ngMod.NoteGroupingTranscriber() - noteGrouping = self._groupingDict.get(self.currentGroupingKey) + if self.currentGroupingKey is None: + raise ValueError('currentGroupingKey must not be None to call extractNoteGrouping') + noteGrouping = self._groupingDict[self.currentGroupingKey] showLeadingOctave = self.showLeadingOctaveFromNoteGrouping(noteGrouping) transcriber.showLeadingOctave = showLeadingOctave @@ -1979,7 +1986,9 @@ def getRawSegments(music21Part, return allSegments -def extractBrailleElements(music21MeasureOrVoice: stream.Measure | stream.Voice): +def extractBrailleElements( + music21MeasureOrVoice: stream.Measure | stream.Voice +) -> BrailleElementGrouping: ''' Takes in a :class:`~music21.stream.Measure` or :class:`~music21.stream.Voice` and returns a :class:`~music21.braille.segment.BrailleElementGrouping` of correctly ordered diff --git a/music21/layout.py b/music21/layout.py index e8c65940c8..c495996a8b 100644 --- a/music21/layout.py +++ b/music21/layout.py @@ -499,7 +499,11 @@ def _setSymbol(self, value: t.Literal['bracket', 'line', 'brace', 'square'] | No # --------------------------------------------------------------- # Stream subclasses for layout -def divideByPages(scoreIn, printUpdates=False, fastMeasures=False): +def divideByPages( + scoreIn: stream.Score, + printUpdates: bool = False, + fastMeasures: bool = False +) -> LayoutScore: ''' Divides a score into a series of smaller scores according to page breaks. Only searches for PageLayout.isNew or SystemLayout.isNew @@ -667,7 +671,8 @@ def getRichSystemLayout(inner_allSystemLayouts): staffObject.pageNumber = pageNumber staffObject.pageSystemNumber = pageSystemNumber - staffObject.elements = p + # until getters/setters can have different types + staffObject.elements = p # type: ignore thisSystem.replace(p, staffObject) allStaffLayouts: list[StaffLayout] = list(p[StaffLayout]) if not allStaffLayouts: diff --git a/music21/metadata/__init__.py b/music21/metadata/__init__.py index e9f090156e..1d188384df 100755 --- a/music21/metadata/__init__.py +++ b/music21/metadata/__init__.py @@ -864,6 +864,13 @@ def __getitem__(self, ]) -> tuple[Text, ...]: pass + @overload + def __getitem__(self, + key: t.Literal[ + 'copyright', + ]) -> tuple[Copyright, ...]: + pass + @overload def __getitem__(self, key: str) -> tuple[Text, ...]: pass diff --git a/music21/musicxml/m21ToXml.py b/music21/musicxml/m21ToXml.py index c4c555c606..e7025bb35a 100644 --- a/music21/musicxml/m21ToXml.py +++ b/music21/musicxml/m21ToXml.py @@ -72,7 +72,7 @@ # ------------------------------------------------------------------------------ -def typeToMusicXMLType(value): +def typeToMusicXMLType(value: str) -> str: ''' Convert a music21 type to a MusicXML type. @@ -113,7 +113,7 @@ def typeToMusicXMLType(value): return value -def normalizeColor(color): +def normalizeColor(color: str) -> str: ''' Normalize a css3 name to hex or leave it alone... @@ -124,8 +124,8 @@ def normalizeColor(color): >>> musicxml.m21ToXml.normalizeColor('#00ff00') '#00FF00' ''' - if color in (None, ''): - return color + if not color: + return '' if '#' not in color: return webcolors.name_to_hex(color).upper() else: @@ -166,14 +166,14 @@ def getMetadataFromContext(s: stream.Stream) -> metadata.Metadata | None: def _setTagTextFromAttribute( - m21El, + m21El: t.Any, xmlEl: Element, tag: str, attributeName: str | None = None, *, - transform=None, - forceEmpty=False -): + transform: t.Callable[[t.Any], t.Any] | None = None, + forceEmpty: bool = False +) -> Element | None: ''' If m21El has an attribute called attributeName, create a new SubElement for xmlEl and set its text to the value of the m21El attribute. @@ -234,7 +234,13 @@ def _setTagTextFromAttribute( return subElement -def _setAttributeFromAttribute(m21El, xmlEl, xmlAttributeName, attributeName=None, transform=None): +def _setAttributeFromAttribute( + m21El: t.Any, + xmlEl: Element, + xmlAttributeName: str, + attributeName: str | None = None, + transform: t.Callable[[t.Any], t.Any] | None = None +): ''' If m21El has at least one element of tag==tag with some text. If it does, set the attribute either with the same name (with "foo-bar" changed to @@ -356,11 +362,11 @@ class GeneralObjectExporter: ('Music21Object', 'fromMusic21Object'), ]) - def __init__(self, obj=None): + def __init__(self, obj: prebase.ProtoM21Object | None = None): self.generalObj = obj self.makeNotation: bool = True - def parse(self, obj=None): + def parse(self, obj: prebase.ProtoM21Object | None = None) -> bytes: r''' Return a bytes object representation of anything from a Score to a single pitch. @@ -430,6 +436,9 @@ def parse(self, obj=None): ''' if obj is None: obj = self.generalObj + if obj is None: + raise MusicXMLExportException('Must have an object to export') + if self.makeNotation: outObj = self.fromGeneralObject(obj) return self.parseWellformedObject(outObj) @@ -438,7 +447,7 @@ def parse(self, obj=None): raise MusicXMLExportException('Can only export Scores with makeNotation=False') return self.parseWellformedObject(obj) - def parseWellformedObject(self, sc) -> bytes: + def parseWellformedObject(self, sc: stream.Score) -> bytes: ''' Parse an object that has already gone through the `.fromGeneralObject` conversion, which has produced a copy with @@ -451,7 +460,7 @@ def parseWellformedObject(self, sc) -> bytes: scoreExporter.parse() return scoreExporter.asBytes() - def fromGeneralObject(self, obj): + def fromGeneralObject(self, obj: prebase.ProtoM21Object): ''' Converts any Music21Object (or a duration or a pitch) to something that can be passed to ScoreExporter() @@ -806,8 +815,8 @@ class XMLExporterBase: contains functions that could be called at multiple levels of exporting (Score, Part, Measure). ''' - def __init__(self): - self.xmlRoot = None + def __init__(self) -> None: + self.xmlRoot = Element('override-me-in-subclasses') self.stream: stream.Stream | None = None def asBytes(self, noCopy=True) -> bytes: @@ -1459,9 +1468,9 @@ def __init__(self, score: stream.Score | None = None, makeNotation: bool = True) self.stream = score self.xmlRoot = Element('score-partwise', version=defaults.musicxmlVersion) - self.mxIdentification = None + self.mxIdentification: Element | None = None - self.scoreMetadata = None + self.scoreMetadata: metadata.Metadata | None = None self.spannerBundle: spanner.SpannerBundle | None = None self.meterStream: stream.Stream[meter.TimeSignatureBase] | None = None @@ -1748,7 +1757,7 @@ def setPartExporterStaffGroups(self): partExp.staffGroup = joinableGroup - def renumberVoicesWithinStaffGroups(self): + def renumberVoicesWithinStaffGroups(self) -> None: ''' Renumbers voices (as appropriate) in each StaffGroup, so that voices have unique numbers across the entire group. @@ -1767,8 +1776,12 @@ def renumberVoicesWithinStaffGroups(self): # renumber the voices in this StaffGroup staffGroupScore = stream.Score(partExp.staffGroup.getSpannedElements()) - measuresStream = staffGroupScore.recurse().getElementsByClass(stream.Measure).stream() - for measureStack in OffsetIterator(measuresStream): + measuresStream: stream.Stream[stream.Measure] = ( + staffGroupScore.recurse().getElementsByClass(stream.Measure).stream() + ) + offsetIterator: OffsetIterator[stream.Measure] = OffsetIterator(measuresStream) + measureStack: t.Sequence[stream.Stream[stream.Measure]] + for measureStack in offsetIterator: nextVoiceId: int = 1 for m in measureStack: for v in m[stream.Voice]: @@ -2273,7 +2286,7 @@ def staffGroupToXmlPartGroup(self, staffGroup): # environLocal.printDebug(['configureMxPartGroupFromStaffGroup: mxPartGroup', mxPartGroup]) return mxPartGroup - def setIdentification(self): + def setIdentification(self) -> Element: # noinspection SpellCheckingInspection, PyShadowingNames ''' Returns an identification object from self.scoreMetadata. And appends to the score... @@ -2400,6 +2413,9 @@ def metadataToMiscellaneous( elif md is None: md = self.scoreMetadata + if t.TYPE_CHECKING: + assert md is not None + mxMiscellaneous = Element('miscellaneous') foundOne = False @@ -2562,6 +2578,9 @@ def setTitles(self) -> None: mdObj = self.scoreMetadata if self.scoreMetadata is None: mdObj = metadata.Metadata() + if t.TYPE_CHECKING: + assert mdObj is not None + mxScoreHeader = self.xmlRoot mxWork = Element('work') # TODO: work-number @@ -2582,7 +2601,7 @@ def setTitles(self) -> None: # musicxml often defaults to show only movement title # if no movementName is found in mdObj, set movement title to - # the mdObj's first title instead. Fall back to defaults.title if + # first title of the mdObj instead. Fall back to defaults.title if # necessary (and if possible). movement_title: str = '' @@ -4610,7 +4629,7 @@ def noteheadToXml(self, n: note.NotRest) -> Element: setb(n, mxNotehead, 'parentheses', 'noteheadParenthesis', transform=xmlObjects.booleanToYesNo) # TODO: font - if n.hasStyleInformation and n.style.color not in (None, ''): + if n.hasStyleInformation and n.style.color: color = normalizeColor(n.style.color) mxNotehead.set('color', color) return mxNotehead diff --git a/music21/musicxml/partStaffExporter.py b/music21/musicxml/partStaffExporter.py index 5c7c6ea1be..8724713464 100644 --- a/music21/musicxml/partStaffExporter.py +++ b/music21/musicxml/partStaffExporter.py @@ -829,23 +829,23 @@ def getRootForPartStaff(self, partStaff: stream.PartStaff) -> Element: not found in self.partExporterList ''' for pex in self.partExporterList: - if partStaff is pex.stream: + if partStaff is pex.stream and pex.xmlRoot is not None: return pex.xmlRoot # now try derivations: for pex in self.partExporterList: for derived in pex.stream.derivation.chain(): - if derived is partStaff: + if derived is partStaff and pex.xmlRoot is not None: return pex.xmlRoot # now just match on id: for pex in self.partExporterList: - if partStaff.id == pex.stream.id: + if partStaff.id == pex.stream.id and pex.xmlRoot is not None: return pex.xmlRoot for pex in self.partExporterList: for derived in pex.stream.derivation.chain(): - if partStaff.id == derived.id: + if partStaff.id == derived.id and pex.xmlRoot is not None: return pex.xmlRoot raise MusicXMLExportException( diff --git a/music21/search/lyrics.py b/music21/search/lyrics.py index d15ee81b70..e1e929fccd 100644 --- a/music21/search/lyrics.py +++ b/music21/search/lyrics.py @@ -16,14 +16,13 @@ from collections import namedtuple, OrderedDict import re import typing as t -from typing import TYPE_CHECKING import unittest from music21.exceptions21 import Music21Exception from music21 import note # from music21 import common -if TYPE_CHECKING: +if t.TYPE_CHECKING: from music21.common.types import StreamType LINEBREAK_TOKEN = ' // ' diff --git a/music21/stream/base.py b/music21/stream/base.py index b5551e21e7..049d3f232f 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -4319,12 +4319,12 @@ def getElementAfterElement(self, element, classList=None): # _getNotes and _getPitches are found with the interval routines def _getMeasureNumberListByStartEnd( self, - numberStart, - numberEnd, + numberStart: int | str, + numberEnd: int | str, *, indicesNotNumbers: bool ) -> list[Measure]: - def hasMeasureNumberInformation(measureIterator): + def hasMeasureNumberInformation(measureIterator: iterator.StreamIterator[Measure]) -> bool: ''' Many people create streams where every number is zero. This will check for that as quickly as possible. @@ -4344,6 +4344,10 @@ def hasMeasureNumberInformation(measureIterator): # FIND THE CORRECT ORIGINAL MEASURE OBJECTS # for indicesNotNumbers, this is simple... if indicesNotNumbers: + if not isinstance(numberStart, int) or not isinstance(numberEnd, int): + raise ValueError( + 'numberStart and numberEnd must be integers with indicesNotNumbers=True' + ) # noinspection PyTypeChecker return t.cast(list[Measure], list(mStreamIter[numberStart:numberEnd])) @@ -4405,13 +4409,15 @@ def hasMeasureNumberInformation(measureIterator): matches.append(m) return matches - def measures(self, - numberStart, - numberEnd, - *, - collect=('Clef', 'TimeSignature', 'Instrument', 'KeySignature'), - gatherSpanners=GatherSpanners.ALL, - indicesNotNumbers=False): + def measures( + self, + numberStart, + numberEnd, + *, + collect=('Clef', 'TimeSignature', 'Instrument', 'KeySignature'), + gatherSpanners=GatherSpanners.ALL, + indicesNotNumbers=False + ) -> Stream[Measure]: ''' Get a region of Measures based on a start and end Measure number where the boundary numbers are both included. @@ -4585,7 +4591,7 @@ def measures(self, ''' startMeasure: Measure | None - returnObj = self.cloneEmpty(derivationMethod='measures') + returnObj = t.cast(Stream[Measure], self.cloneEmpty(derivationMethod='measures')) srcObj = self matches = self._getMeasureNumberListByStartEnd( @@ -4594,9 +4600,10 @@ def measures(self, indicesNotNumbers=indicesNotNumbers ) + startOffset: OffsetQL if not matches: startMeasure = None - startOffset = 0 # does not matter; could be any number... + startOffset = 0.0 # does not matter; could be any number... else: startMeasure = matches[0] startOffset = startMeasure.getOffsetBySite(srcObj) diff --git a/music21/stream/core.py b/music21/stream/core.py index db5d06b873..afd2b32f53 100644 --- a/music21/stream/core.py +++ b/music21/stream/core.py @@ -566,9 +566,9 @@ def asTree(self, *, flatten=False, classList=None, useTimespans=False, groupOffs def coreGatherMissingSpanners( self, *, - recurse=True, - requireAllPresent=True, - insert=True, + recurse: bool = True, + requireAllPresent: bool = True, + insert: bool = True, constrainingSpannerBundle: spanner.SpannerBundle | None = None ) -> list[spanner.Spanner] | None: ''' diff --git a/music21/stream/tests.py b/music21/stream/tests.py index 088bb3dc48..65cb3e45e5 100644 --- a/music21/stream/tests.py +++ b/music21/stream/tests.py @@ -1728,17 +1728,17 @@ def testMeasureOffsetMap(self): m1 = a.parts[0].getElementsByClass(Measure)[1] # m1.show('text') - mOffsetMap = m1.measureOffsetMap(note.Note) + mOffsetMap = m1.measureOffsetMap([note.Note]) # offset here is that of measure that originally contained this note # environLocal.printDebug(['m1', m1, 'mOffsetMap', mOffsetMap]) self.assertEqual(sorted(list(mOffsetMap.keys())), [4.0]) m2 = a.parts[0].getElementsByClass(Measure)[2] - mOffsetMap = m2.measureOffsetMap(note.Note) + mOffsetMap = m2.measureOffsetMap([note.Note]) # offset here is that of measure that originally contained this note self.assertEqual(sorted(list(mOffsetMap.keys())), [8.0]) - mOffsetMap = a.flatten().measureOffsetMap('Note') + mOffsetMap = a.flatten().measureOffsetMap(['Note']) self.assertEqual(sorted(mOffsetMap.keys()), [0.0, 4.0, 8.0, 12.0, 16.0, 20.0, 24.0, 34.0, 38.0]) diff --git a/music21/tree/spans.py b/music21/tree/spans.py index cae6775ac4..b5afc62bcc 100644 --- a/music21/tree/spans.py +++ b/music21/tree/spans.py @@ -19,14 +19,14 @@ import copy from math import inf -from typing import TYPE_CHECKING +import typing as t import unittest from music21.common.types import OffsetQLIn from music21 import environment from music21 import exceptions21 -if TYPE_CHECKING: +if t.TYPE_CHECKING: from music21 import base from music21 import stream diff --git a/music21/tree/verticality.py b/music21/tree/verticality.py index 493ab527fa..f817b6a428 100644 --- a/music21/tree/verticality.py +++ b/music21/tree/verticality.py @@ -19,7 +19,7 @@ from collections.abc import Iterable, Sequence import copy import itertools -from typing import TYPE_CHECKING +import typing as t import unittest from music21 import chord @@ -887,7 +887,7 @@ def conditionalAdd(ts, n: note.Note) -> None: for subEl in list(el)[1:]: conditionalAdd(timeSpan, subEl) else: - if TYPE_CHECKING: + if t.TYPE_CHECKING: assert isinstance(el, note.Note) conditionalAdd(timeSpan, el) From 089109e5e11551a590b84acea94cb63b3f92863b Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Mon, 5 Dec 2022 22:39:11 -1000 Subject: [PATCH 07/16] another fix --- music21/base.py | 4 ++-- music21/chord/__init__.py | 6 +++--- music21/graph/plot.py | 10 +++++++++- music21/graph/primitives.py | 2 +- music21/metadata/__init__.py | 9 +++++++-- music21/note.py | 10 ++++++---- music21/stream/base.py | 3 ++- 7 files changed, 30 insertions(+), 14 deletions(-) diff --git a/music21/base.py b/music21/base.py index 457fad51e0..8a5bd77e00 100644 --- a/music21/base.py +++ b/music21/base.py @@ -859,7 +859,7 @@ def derivation(self) -> Derivation: def derivation(self, newDerivation: Derivation | None) -> None: self._derivation = newDerivation - def clearCache(self, **keywords): + def clearCache(self, **keywords) -> None: ''' A number of music21 attributes (especially with Chords and RomanNumerals, etc.) are expensive to compute and are therefore cached. Generally speaking @@ -879,7 +879,7 @@ def clearCache(self, **keywords): ''' # do not replace with self._cache.clear() -- leaves terrible # state for shallow copies. - self._cache: dict[str, t.Any] = {} + self._cache = {} @overload def getOffsetBySite( diff --git a/music21/chord/__init__.py b/music21/chord/__init__.py index c7f7359be5..bcce327919 100644 --- a/music21/chord/__init__.py +++ b/music21/chord/__init__.py @@ -1720,7 +1720,7 @@ def containsTriad(self) -> bool: return True - def _findRoot(self): + def _findRoot(self) -> pitch.Pitch: ''' Looks for the root usually by finding the note with the most 3rds above it. @@ -2539,7 +2539,7 @@ def _findInversion(self, rootPitch: pitch.Pitch) -> int: self._cache['inversion'] = inv return inv - def inversionName(self): + def inversionName(self) -> int | None: ''' Returns an integer representing the common abbreviation for the inversion the chord is in. If chord is not in a common inversion, @@ -4755,7 +4755,7 @@ def chordTablesAddress(self): @property # type: ignore @cacheMethod - def commonName(self): + def commonName(self) -> str: ''' Return the most common name associated with this Chord as a string. Checks some common enharmonic equivalents. diff --git a/music21/graph/plot.py b/music21/graph/plot.py index d87b53fe30..bbb65656f4 100644 --- a/music21/graph/plot.py +++ b/music21/graph/plot.py @@ -1039,10 +1039,18 @@ class HorizontalBar(primitives.GraphHorizontalBar, PlotStreamMixin): 'y': axis.PitchSpaceAxis, } - def __init__(self, streamObj=None, *, colorByPart=False, **keywords): + def __init__( + self, + streamObj: stream.Stream | None = None, + *, + colorByPart=False, + **keywords + ) -> None: self.colorByPart = colorByPart self._partsToColor: dict[stream.Part, str] = {} + self.axisY: axis.PitchSpaceAxis + primitives.GraphHorizontalBar.__init__(self, **keywords) PlotStreamMixin.__init__(self, streamObj, **keywords) diff --git a/music21/graph/primitives.py b/music21/graph/primitives.py index 1a33605ba7..fb5e324037 100644 --- a/music21/graph/primitives.py +++ b/music21/graph/primitives.py @@ -980,7 +980,7 @@ def __init__(self, **keywords): def barHeight(self): return self.barSpace - (self.margin * 2) - def renderSubplot(self, subplot): + def renderSubplot(self, subplot) -> None: self.figure.subplots_adjust(left=0.15) yPos = 0 diff --git a/music21/metadata/__init__.py b/music21/metadata/__init__.py index 1d188384df..68a9529c86 100755 --- a/music21/metadata/__init__.py +++ b/music21/metadata/__init__.py @@ -1038,7 +1038,12 @@ def getContributorsByRole(self, role: str | None) -> tuple[Contributor, ...]: result.append(contrib) return tuple(result) - def search(self, query=None, field=None, **keywords): + def search( + self, + query=None, + field=None, + **keywords + ): r''' Search one or all fields with a query, given either as a string or a regular expression match. @@ -1654,7 +1659,7 @@ def title(self, value: str) -> None: setattr(self, 'title', value) @property - def bestTitle(self): + def bestTitle(self) -> str | None: r''' Get the title of the work, or the next-matched title string available from a related parameter fields. diff --git a/music21/note.py b/music21/note.py index 64a79d7ef1..4e802506ea 100644 --- a/music21/note.py +++ b/music21/note.py @@ -1813,9 +1813,11 @@ class Unpitched(NotRest): equalityAttributes = ('displayStep', 'displayOctave') - def __init__(self, - displayName=None, - **keywords): + def __init__( + self, + displayName: str | None = None, + **keywords + ): super().__init__(**keywords) self._chordAttached: percussion.PercussionChord | None = None @@ -1824,7 +1826,7 @@ def __init__(self, if displayName: display_pitch = Pitch(displayName) self.displayStep = display_pitch.step - self.displayOctave = display_pitch.octave + self.displayOctave = display_pitch.implicitOctave def _getStoredInstrument(self): return self._storedInstrument diff --git a/music21/stream/base.py b/music21/stream/base.py index 049d3f232f..72c4f9fd0c 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -30,6 +30,7 @@ from math import isclose import os import pathlib +import types import typing as t from typing import overload # pycharm bug disallows alias import unittest @@ -4344,7 +4345,7 @@ def hasMeasureNumberInformation(measureIterator: iterator.StreamIterator[Measure # FIND THE CORRECT ORIGINAL MEASURE OBJECTS # for indicesNotNumbers, this is simple... if indicesNotNumbers: - if not isinstance(numberStart, int) or not isinstance(numberEnd, int): + if not isinstance(numberStart, int) or not isinstance(numberEnd, (int, types.NoneType)): raise ValueError( 'numberStart and numberEnd must be integers with indicesNotNumbers=True' ) From 74e9d5520d49f7bbf426a93196b75f06dccbbe67 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Mon, 5 Dec 2022 23:43:40 -1000 Subject: [PATCH 08/16] more fixes -- rename DeltaTime.read --- music21/defaults.py | 4 +- music21/expressions.py | 2 +- music21/features/base.py | 5 +- music21/instrument.py | 22 ++++---- music21/metadata/__init__.py | 22 ++++---- music21/metadata/bundles.py | 6 ++- music21/midi/__init__.py | 24 ++++----- music21/midi/translate.py | 28 +++++++---- music21/musicxml/test_xmlToM21.py | 7 +-- music21/musicxml/xmlObjects.py | 2 +- music21/musicxml/xmlToM21.py | 83 +++++++++++++++++++++---------- music21/romanText/tsvConverter.py | 69 ++++++++++++------------- music21/scale/__init__.py | 16 +++--- music21/stream/base.py | 2 +- 14 files changed, 169 insertions(+), 123 deletions(-) diff --git a/music21/defaults.py b/music21/defaults.py index 4f144910f7..7452e866b6 100644 --- a/music21/defaults.py +++ b/music21/defaults.py @@ -49,8 +49,8 @@ durationType = 'quarter' -instrumentName = '' -partName = '' +instrumentName: str = '' +partName: str = '' keyFifths = 0 keyMode = 'major' diff --git a/music21/expressions.py b/music21/expressions.py index 1950203632..35550ba9d7 100644 --- a/music21/expressions.py +++ b/music21/expressions.py @@ -736,7 +736,7 @@ class Trill(Ornament): * Changed in v7: the size should be a generic second. ''' - def __init__(self, **keywords): + def __init__(self, **keywords) -> None: super().__init__(**keywords) self.size: interval.IntervalBase = interval.GenericInterval(2) diff --git a/music21/features/base.py b/music21/features/base.py index 4c235a3477..e4c64eff4b 100644 --- a/music21/features/base.py +++ b/music21/features/base.py @@ -135,7 +135,10 @@ class FeatureExtractor: Usage of a DataInstance offers significant performance advantages, as common forms of the Stream are cached for easy processing. ''' - def __init__(self, dataOrStream=None, **keywords): + def __init__(self, + dataOrStream=None, + **keywords + ) -> None: self.stream = None # the original Stream, or None self.data: DataInstance | None = None # a DataInstance object: use to get data self.setData(dataOrStream) diff --git a/music21/instrument.py b/music21/instrument.py index f11b1406d4..1ece83b862 100644 --- a/music21/instrument.py +++ b/music21/instrument.py @@ -150,29 +150,29 @@ class Instrument(base.Music21Object): ''' classSortOrder = -25 - def __init__(self, instrumentName=None, **keywords): + def __init__(self, instrumentName: str | None = None, **keywords): super().__init__(**keywords) - self.partId = None + self.partId: str | None = None self._partIdIsRandom = False - self.partName = None - self.partAbbreviation = None + self.partName: str | None = None + self.partAbbreviation: str | None = None - self.printPartName = None # True = yes, False = no, None = let others decide - self.printPartAbbreviation = None + self.printPartName: bool | None = None # True = yes, False = no, None = let others decide + self.printPartAbbreviation: bool | None = None self.instrumentId: str | None = None # apply to midi and instrument self._instrumentIdIsRandom = False - self.instrumentName: str = instrumentName + self.instrumentName: str | None = instrumentName self.instrumentAbbreviation: str | None = None self.midiProgram: int | None = None # 0-indexed self.midiChannel: int | None = None # 0-indexed self.instrumentSound: str | None = None - self.lowestNote = None - self.highestNote = None + self.lowestNote: pitch.Pitch | None = None + self.highestNote: pitch.Pitch | None = None # define interval to go from written to sounding self.transposition: interval.Interval | None = None @@ -2245,7 +2245,7 @@ def partitionByInstrument(streamObj: stream.Stream) -> stream.Stream: for instrumentObj in instrumentIterator: # matching here by instrument name if instrumentObj.instrumentName not in names: - names[instrumentObj.instrumentName] = {'Instrument': instrumentObj} + names[instrumentObj.instrumentName or ''] = {'Instrument': instrumentObj} # just store one instance # create a return object that has a part for each instrument @@ -2552,7 +2552,7 @@ def getAllNamesForInstrument(instrumentClass: Instrument, language = language.lower() instrumentNameDict = {} - instrumentClassName = instrumentClass.instrumentName + instrumentClassName = instrumentClass.instrumentName or '' if language == SearchLanguage.ALL: for lang in SearchLanguage: diff --git a/music21/metadata/__init__.py b/music21/metadata/__init__.py index 68a9529c86..5945b9e173 100755 --- a/music21/metadata/__init__.py +++ b/music21/metadata/__init__.py @@ -236,9 +236,9 @@ class Metadata(base.Music21Object): # INITIALIZER # - def __init__(self, **keywords): - m21BaseKeywords = {} - myKeywords = {} + def __init__(self, **keywords) -> None: + m21BaseKeywords: dict[str, t.Any] = {} + myKeywords: dict[str, t.Any] = {} # We allow the setting of metadata values (attribute-style) via **keywords. # Any keywords that are uniqueNames, grandfathered workIds, or grandfathered @@ -1040,10 +1040,10 @@ def getContributorsByRole(self, role: str | None) -> tuple[Contributor, ...]: def search( self, - query=None, - field=None, + query: str | t.Pattern | t.Callable[[str], bool] | None = None, + field: str | None = None, **keywords - ): + ) -> tuple[bool, str | None]: r''' Search one or all fields with a query, given either as a string or a regular expression match. @@ -1109,7 +1109,7 @@ def search( # TODO: Change to a namedtuple and add as a third element # during a successful search, the full value of the retrieved # field (so that 'Joplin' would return 'Joplin, Scott') - reQuery = None + reQuery: t.Pattern | None = None valueFieldPairs = [] if query is None and field is None and not keywords: return (False, None) @@ -1181,7 +1181,7 @@ def search( # ultimately, can look for regular expressions by checking for # .search useRegex = False - if hasattr(query, 'search'): + if isinstance(query, t.Pattern): useRegex = True reQuery = query # already compiled # look for regex characters @@ -1190,12 +1190,12 @@ def search( useRegex = True reQuery = re.compile(query, flags=re.IGNORECASE) - if useRegex: + if useRegex and reQuery is not None: for value, innerField in valueFieldPairs: # "re.IGNORECASE" makes case-insensitive search if isinstance(value, str): - match = reQuery.search(value) - if match is not None: + matchReSearch = reQuery.search(value) + if matchReSearch is not None: return True, innerField elif callable(query): for value, innerField in valueFieldPairs: diff --git a/music21/metadata/bundles.py b/music21/metadata/bundles.py index 7e3753d8b4..71b9c464c0 100644 --- a/music21/metadata/bundles.py +++ b/music21/metadata/bundles.py @@ -268,13 +268,15 @@ class MetadataBundle(prebase.ProtoM21Object): # INITIALIZER # - def __init__(self, expr=None): + def __init__(self, expr: 'music21.corpus.corpora.Corpus' | str | None = None): from music21 import corpus + self._metadataEntries: OrderedDict[str, MetadataEntry] = OrderedDict() if not isinstance(expr, (str, corpus.corpora.Corpus, type(None))): raise MetadataBundleException('Need to take a string, corpus, or None as expression') - self._corpus = None + self._corpus: corpus.corpora.Corpus | None = None + self._name: str | None if isinstance(expr, corpus.corpora.Corpus): self._name = expr.name diff --git a/music21/midi/__init__.py b/music21/midi/__init__.py index 8acab05d96..ac5af1bbdd 100644 --- a/music21/midi/__init__.py +++ b/music21/midi/__init__.py @@ -842,7 +842,7 @@ def parseChannelVoiceMessage(self, midiBytes: bytes) -> bytes: return midiBytes[3:] raise TypeError(f'expected ChannelVoiceMessage, got {self.type}') # pragma: no cover - def read(self, midiBytes): + def read(self, midiBytes: bytes) -> bytes: r''' Parse the bytes given and take the beginning section and convert it into data for this event and return the @@ -853,7 +853,6 @@ def read(self, midiBytes): >>> hex(noteOnMessage) '0x92' - This is how the system reads note-on messages (0x90-0x9F) and channels >>> hex(0x91 & 0xF0) # testing message type extraction @@ -1060,7 +1059,6 @@ def isDeltaTime(self): ''' Return a boolean if this is a DeltaTime subclass. - >>> mt = midi.MidiTrack(1) >>> dt = midi.DeltaTime(mt) >>> dt.isDeltaTime() @@ -1154,7 +1152,7 @@ def _reprInternal(self): rep = '(empty) ' + rep return rep - def read(self, oldBytes: bytes) -> tuple[int, bytes]: + def readUntilLowByte(self, oldBytes: bytes) -> tuple[int, bytes]: r''' Read a byte-string until hitting a character below 0x80 and return the converted number and the rest of the bytes @@ -1274,10 +1272,10 @@ def __init__(self, index=0): def length(self): return len(self.data) - def read(self, midiBytes): + def read(self, midiBytes: bytes) -> bytes: ''' - Read as much of the string (representing midi data) as necessary; - return the remaining string for reassignment and further processing. + Read as much of the bytes object (representing midi data) as necessary; + return the remaining bytes object for reassignment and further processing. The string should begin with `MTrk`, specifying a Midi Track @@ -1299,7 +1297,7 @@ def read(self, midiBytes): self.processDataToEvents(trackData) return remainder # remainder string after extracting track data - def processDataToEvents(self, trackData: bytes = b''): + def processDataToEvents(self, trackData: bytes = b'') -> None: ''' Populate .events with trackData. Called by .read() ''' @@ -1309,7 +1307,7 @@ def processDataToEvents(self, trackData: bytes = b''): # shave off the time stamp from the event delta_t = DeltaTime(track=self) # return extracted time, as well as remaining bytes - dt, trackDataCandidate = delta_t.read(trackData) + dt, trackDataCandidate = delta_t.readUntilLowByte(trackData) # this is the offset that this event happens at, in ticks timeCandidate = time + dt @@ -1763,10 +1761,10 @@ def testBasicImport(self): mf.write() mf.close() -# mf = MidiFile() -# mf.open(fp) -# mf.read() -# mf.close() + # mf = MidiFile() + # mf.open(fp) + # mf.read() + # mf.close() def testInternalDataModel(self): dirLib = common.getSourceFilePath() / 'midi' / 'testPrimitive' diff --git a/music21/midi/translate.py b/music21/midi/translate.py index ba1d7b2270..143c0a2e2d 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -824,7 +824,9 @@ def instrumentToMidiEvents(inputM21, # ------------------------------------------------------------------------------ # Meta events -def midiEventsToInstrument(eventList): +def midiEventsToInstrument( + eventList: midi.MidiEvent | tuple[int, midi.MidiEvent] +) -> instrument.Instrument: ''' Convert a single MIDI event into a music21 Instrument object. @@ -848,7 +850,7 @@ def midiEventsToInstrument(eventList): from music21 import midi as midiModule if not common.isListLike(eventList): - event = eventList + event = t.cast(midi.MidiEvent, eventList) else: # get the second event; first is delta time event = eventList[1] @@ -1815,10 +1817,12 @@ def getNotesFromEvents( return notes -def getMetaEvents(events): +def getMetaEvents( + events: list[tuple[int, midi.MidiEvent]] +) -> list[tuple[int, base.Music21Object]]: from music21.midi import MetaEvents, ChannelVoiceMessages - metaEvents = [] # store pairs of abs time, m21 object + metaEvents: list[tuple[int, base.Music21Object]] = [] # store pairs of abs time, m21 object last_program: int = -1 for eventTuple in events: timeEvent, e = eventTuple @@ -3771,34 +3775,38 @@ def testMidiExportVelocityA(self): self.assertEqual(mtsRepr.count('velocity=114'), 1) self.assertEqual(mtsRepr.count('velocity=13'), 1) - def testMidiExportVelocityB(self): + def testMidiExportVelocityB(self) -> None: import random from music21 import volume - s1 = stream.Stream() + s1: stream.Stream = stream.Stream() shift = [0, 6, 12] amps = [(x / 10. + 0.4) for x in range(6)] amps = amps + list(reversed(amps)) qlList = [1.5] * 6 + [1] * 8 + [2] * 6 + [1.5] * 8 + [1] * 4 + + c: note.Rest | chord.Chord for j, ql in enumerate(qlList): if random.random() > 0.6: c = note.Rest() else: - c = chord.Chord(['c3', 'd-4', 'g5']) + ch = chord.Chord(['c3', 'd-4', 'g5']) vChord: list[volume.Volume] = [] - for i, unused_cSub in enumerate(c): + for i, unused_cSub in enumerate(ch): v = volume.Volume() v.velocityScalar = amps[(j + shift[i]) % len(amps)] v.velocityIsRelative = False vChord.append(v) - c.setVolumes(vChord) + ch.setVolumes(vChord) + c = ch c.duration.quarterLength = ql s1.append(c) - s2 = stream.Stream() random.shuffle(qlList) random.shuffle(amps) + + s2: stream.Stream[note.Note] = stream.Stream() for j, ql in enumerate(qlList): n = note.Note(random.choice(['f#2', 'f#2', 'e-2'])) n.duration.quarterLength = ql diff --git a/music21/musicxml/test_xmlToM21.py b/music21/musicxml/test_xmlToM21.py index a5163cc500..d2a56724e3 100644 --- a/music21/musicxml/test_xmlToM21.py +++ b/music21/musicxml/test_xmlToM21.py @@ -1,6 +1,7 @@ from __future__ import annotations import fractions +import typing as t import unittest import xml.etree.ElementTree as ET @@ -1187,16 +1188,16 @@ def testArpeggioMarks(self): gnote_index += 1 - def testArpeggioMarkSpanners(self): + def testArpeggioMarkSpanners(self) -> None: from music21 import converter from music21.musicxml import testPrimitive - s = converter.parse(testPrimitive.multiStaffArpeggios) + s = t.cast(stream.Score, converter.parse(testPrimitive.multiStaffArpeggios)) sb = s.spannerBundle.getByClass(expressions.ArpeggioMarkSpanner) self.assertIsNotNone(sb) sp = sb[0] # go find all the chords and check for spanner vs expressions - chords: [chord.Chord] = [] + chords: list[chord.Chord] = [] for i, p in enumerate(s.parts): # ArpeggioMarkSpanner spans the second chord (index == 1) across both parts chords.append(p[chord.Chord][1]) diff --git a/music21/musicxml/xmlObjects.py b/music21/musicxml/xmlObjects.py index b9524045e3..59e50b5a96 100644 --- a/music21/musicxml/xmlObjects.py +++ b/music21/musicxml/xmlObjects.py @@ -105,7 +105,7 @@ # ------------------------------------------------------------------------------ class MusicXMLException(exceptions21.Music21Exception): - def __init__(self, message): + def __init__(self, message: str): super().__init__(message) self.measureNumber: str = '' self.partName: str = '' diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 7c39739e91..0cfc3c0e7b 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -59,6 +59,7 @@ if t.TYPE_CHECKING: from music21 import base + # what goes in a `.staffReference` StaffReferenceType = dict[int, list[base.Music21Object]] @@ -89,7 +90,7 @@ def _clean(badStr: str | None) -> str | None: # Durations -def textNotNone(mxObj): +def textNotNone(mxObj: ET.Element | None) -> bool: ''' returns True is mxObj is not None and mxObj.text is not None @@ -111,7 +112,7 @@ def textNotNone(mxObj): return True -def textStripValid(mxObj: ET.Element): +def textStripValid(mxObj: ET.Element | None) -> bool: ''' returns True if textNotNone(mxObj) and mxObj.text.strip() is not empty @@ -130,7 +131,7 @@ def textStripValid(mxObj: ET.Element): if not textNotNone(mxObj): return False if t.TYPE_CHECKING: - assert mxObj.text is not None + assert mxObj is not None and mxObj.text is not None if not mxObj.text.strip(): return False @@ -3655,7 +3656,7 @@ def xmlGraceToGrace(self, mxGrace, noteOrChord): return post - def xmlNotations(self, mxNotations, n): + def xmlNotations(self, mxNotations: ET.Element, n: note.GeneralNote): # noinspection PyShadowingNames ''' >>> from xml.etree.ElementTree import fromstring as EL @@ -3718,7 +3719,7 @@ def flatten(mx, name): if fermataType is not None: fermata.type = fermataType if textStripValid(mxObj): - fermata.shape = mxObj.text.strip() + fermata.shape = mxObj.text.strip() # type: ignore n.expressions.append(fermata) # get any arpeggios, store in expressions. @@ -3729,7 +3730,7 @@ def flatten(mx, name): if tagSearch == 'non-arpeggiate': arpeggioType = 'non-arpeggio' else: - arpeggioType = mxObj.get('direction') + arpeggioType = mxObj.get('direction') or '' idFound: str | None = mxObj.get('number') if idFound is None: arpeggio = expressions.ArpeggioMark(arpeggioType) @@ -4321,7 +4322,7 @@ def xmlToTie(self, mxNote): tieObj.placement = 'below' return tieObj - def xmlToTuplets(self, mxNote): + def xmlToTuplets(self, mxNote: ET.Element) -> list[duration.Tuplet]: # noinspection PyShadowingNames ''' Given an mxNote, based on mxTimeModification @@ -4350,6 +4351,9 @@ def xmlToTuplets(self, mxNote): ''' tup = duration.Tuplet() mxTimeModification = mxNote.find('time-modification') + if mxTimeModification is None: + raise MusicXMLImportException('Note without time-modification in xmlToTuplets') + # environLocal.printDebug(['got mxTimeModification', mxTimeModification]) # This should only be a backup in case there are no tuplet definitions @@ -4359,10 +4363,11 @@ def xmlToTuplets(self, mxNote): seta(tup, mxTimeModification, 'normal-notes', 'numberNotesNormal', transform=int) mxNormalType = mxTimeModification.find('normal-type') + musicXMLNormalType: str if textStripValid(mxNormalType): - musicXMLNormalType = mxNormalType.text.strip() + musicXMLNormalType = mxNormalType.text.strip() # type: ignore else: - musicXMLNormalType = mxNote.find('type').text.strip() + musicXMLNormalType = mxNote.find('type').text.strip() # type: ignore durationNormalType = musicXMLTypeToType(musicXMLNormalType) numDots = len(mxTimeModification.findall('normal-dot')) @@ -4394,7 +4399,7 @@ def xmlToTuplets(self, mxNote): if this_tuplet_type == 'stop': if self.activeTuplets[tupletIndex] is not None: activeT = self.activeTuplets[tupletIndex] - if activeT in returnTuplets: + if activeT in returnTuplets and activeT is not None: activeT.type = 'startStop' removeFromActiveTuplets.add(tupletIndex) tupletsToStop.add(tupletIndex) @@ -4414,21 +4419,24 @@ def xmlToTuplets(self, mxNote): 'tuplet-number', 'numberNotesNormal', transform=int) mxActualType = mxTupletActual.find('tuplet-type') - if mxActualType is not None: - xmlActualType = mxActualType.text.strip() + if (mxActualType is not None + and (xmlActualType := mxActualType.text) is not None): + xmlActualType = xmlActualType.strip() durType = musicXMLTypeToType(xmlActualType) dots = len(mxActualType.findall('tuplet-dot')) tup.durationActual = duration.durationTupleFromTypeDots(durType, dots) mxNormalType = mxTupletNormal.find('tuplet-type') - if mxNormalType is not None: - xmlNormalType = mxNormalType.text.strip() + if (mxNormalType is not None + and (mxNormalTypeText := mxNormalType.text) is not None): + xmlNormalType = mxNormalTypeText.strip() durType = musicXMLTypeToType(xmlNormalType) dots = len(mxNormalType.findall('tuplet-dot')) tup.durationNormal = duration.durationTupleFromTypeDots(durType, dots) # TODO: combine start + stop into startStop. - tup.type = this_tuplet_type + tup.type = t.cast(t.Literal['start', 'stop', 'startStop', False] | None, + this_tuplet_type) bracketMaybe = mxTuplet.get('bracket') if bracketMaybe is not None: @@ -4453,7 +4461,7 @@ def xmlToTuplets(self, mxNote): if lineShape is not None and lineShape == 'curved': tup.bracket = 'slur' # TODO: default-x, default-y, relative-x, relative-y - tup.placement = mxTuplet.get('placement') + tup.placement = t.cast(t.Literal['above', 'below'], mxTuplet.get('placement')) returnTuplets[tupletIndex] = tup remainingTupletAmountToAccountFor /= tup.tupletMultiplier() self.activeTuplets[tupletIndex] = tup @@ -4914,7 +4922,10 @@ def xmlHarmony(self, mxHarmony): self.insertCoreAndRef(self.offsetMeasureNote + chordOffset, mxHarmony, h) - def xmlToChordSymbol(self, mxHarmony): + def xmlToChordSymbol( + self, + mxHarmony: ET.Element + ) -> harmony.ChordSymbol | harmony.NoChord | tablature.ChordWithFretBoard: # noinspection PyShadowingNames ''' Convert a tag to a harmony.ChordSymbol object: @@ -4970,29 +4981,43 @@ def xmlToChordSymbol(self, mxHarmony): mxKind = mxHarmony.find('kind') if textStripValid(mxKind): - chordKind = mxKind.text.strip() + if t.TYPE_CHECKING: + assert mxKind is not None + mxKindText = mxKind.text + if t.TYPE_CHECKING: + assert mxKindText is not None + chordKind = mxKindText.strip() mxFrame = mxHarmony.find('frame') mxBass = mxHarmony.find('bass') if mxBass is not None: # required - b = pitch.Pitch(mxBass.find('bass-step').text) + bassStep = mxBass.find('bass-step') + if bassStep is None: + raise MusicXMLImportException('bass-step missing') + + b = pitch.Pitch(bassStep.text) # optional mxBassAlter = mxBass.find('bass-alter') - if mxBassAlter is not None: + if mxBassAlter is not None and (alterText := mxBassAlter.text) is not None: # can provide integer or float to create accidental on pitch - b.accidental = pitch.Accidental(float(mxBassAlter.text)) + b.accidental = pitch.Accidental(float(alterText)) # TODO: musicxml 4: bass-separator: use something besides slash on output. mxInversion = mxHarmony.find('inversion') if textStripValid(mxInversion): # TODO: print-style for inversion # TODO: musicxml 4: text attribute overrides display of the inversion. + if t.TYPE_CHECKING: + assert mxInversion is not None and mxInversion.text is not None + inversion = int(mxInversion.text.strip()) # TODO: print-style if chordKind: # two ways of doing it... + if t.TYPE_CHECKING: + assert mxKind is not None # Get m21 chord kind from dict of musicxml aliases ("dominant" -> "dominant-seventh") if chordKind in harmony.CHORD_ALIASES: chordKind = harmony.CHORD_ALIASES[chordKind] @@ -5005,18 +5030,24 @@ def xmlToChordSymbol(self, mxHarmony): mxRoot = mxHarmony.find('root') if mxRoot is not None: # choice: or mxRS = mxRoot.find('root-step') + if t.TYPE_CHECKING: + assert mxRS is not None + rootText = mxRS.text if rootText in (None, ''): rootText = mxRS.get('text') # two ways to do it... this should do display even # if content is supported. - r = pitch.Pitch(rootText) - mxRootAlter = mxRoot.find('root-alter') - if mxRootAlter is not None: - # can provide integer or float to create accidental on pitch - r.accidental = pitch.Accidental(float(mxRootAlter.text)) + if rootText is not None: + r = pitch.Pitch(rootText) + mxRootAlter = mxRoot.find('root-alter') + if mxRootAlter is not None: + # can provide integer or float to create accidental on pitch + alterFloat = float(mxRootAlter.text) # type: ignore + r.accidental = pitch.Accidental(alterFloat) # TODO: musicxml 4: numeral -- pretty important. + cs_class: type[harmony.ChordSymbol | harmony.NoChord | tablature.ChordWithFretBoard] if mxFrame is not None: cs_class = tablature.ChordWithFretBoard elif chordKind == 'none': diff --git a/music21/romanText/tsvConverter.py b/music21/romanText/tsvConverter.py index bae2458fc1..5e7ad949f3 100644 --- a/music21/romanText/tsvConverter.py +++ b/music21/romanText/tsvConverter.py @@ -24,6 +24,7 @@ from music21 import chord from music21 import common +from music21.common.types import OffsetQL from music21 import environment from music21 import harmony from music21 import key @@ -161,29 +162,29 @@ class TabChordBase(abc.ABC): between tabular data and music21 chords. ''' - def __init__(self): + def __init__(self) -> None: super().__init__() - self.numeral = None - self.relativeroot = None - self.representationType = None # Added (not in DCML) + self.numeral: str = '' + self.relativeroot: str | None = None + self.representationType: str | None = None # Added (not in DCML) self.extra: dict[str, str] = {} self.dcml_version = -1 # shared between DCML v1 and v2 - self.chord = None - self.timesig = None - self.pedal = None - self.form = None - self.figbass = None - self.changes = None - self.phraseend = None + self.chord: str = '' + self.timesig: str = '' + self.pedal: str | None = None + self.form: str | None = None + self.figbass: str | None = None + self.changes: str | None = None + self.phraseend: str | None = None # the following attributes are overwritten by properties in TabChordV2 # because of changed column names in DCML v2 - self.local_key = None - self.global_key = None - self.beat = None - self.measure = None + self.local_key: str = '' + self.global_key: str = '' + self.beat: float = 1.0 + self.measure: int = 1 @property def combinedChord(self) -> str: @@ -268,36 +269,36 @@ def _changeRepresentation(self) -> None: if self.relativeroot: # If there's a relative root ... if isMinor(self.relativeroot): # ... and it's minor too, change it and the figure self.relativeroot = characterSwaps(self.relativeroot, - minor=True, - direction=direction) + minor=True, + direction=direction) self.numeral = characterSwaps(self.numeral, - minor=True, - direction=direction) + minor=True, + direction=direction) else: # ... rel. root but not minor self.relativeroot = characterSwaps(self.relativeroot, - minor=False, - direction=direction) + minor=False, + direction=direction) else: # No relative root self.numeral = characterSwaps(self.numeral, - minor=True, - direction=direction) + minor=True, + direction=direction) else: # local key not minor if self.relativeroot: # if there's a relativeroot ... if isMinor(self.relativeroot): # ... and it's minor, change it and the figure self.relativeroot = characterSwaps(self.relativeroot, - minor=False, - direction=direction) + minor=False, + direction=direction) self.numeral = characterSwaps(self.numeral, - minor=True, - direction=direction) + minor=True, + direction=direction) else: # ... rel. root but not minor self.relativeroot = characterSwaps(self.relativeroot, - minor=False, - direction=direction) + minor=False, + direction=direction) else: # No relative root self.numeral = characterSwaps(self.numeral, - minor=False, - direction=direction) + minor=False, + direction=direction) def tabToM21(self) -> harmony.Harmony: ''' @@ -697,7 +698,7 @@ def prepStream(self) -> stream.Score: if entry.timesig != currentTimeSig: newTS = meter.TimeSignature(entry.timesig) m.insert(entry.beat - 1, newTS) - currentTimeSig = entry.timesig + currentTimeSig = entry.timesig or '' currentMeasureLength = newTS.barDuration.quarterLength previousMeasure = entry.measure @@ -771,8 +772,8 @@ def _m21ToTsv_v1(self) -> list[list[str]]: thisEntry.combinedChord = thisRN.figure # NB: slightly different from DCML: no key. thisEntry.altchord = altChord - thisEntry.measure = thisRN.measureNumber - thisEntry.beat = thisRN.beat + thisEntry.measure = thisRN.measureNumber if thisRN.measureNumber is not None else 1 + thisEntry.beat = float(thisRN.beat) thisEntry.totbeat = None ts = thisRN.getContextByClass(meter.TimeSignature) if ts is None: diff --git a/music21/scale/__init__.py b/music21/scale/__init__.py index 85078f81ff..7fc926a454 100644 --- a/music21/scale/__init__.py +++ b/music21/scale/__init__.py @@ -847,9 +847,10 @@ class AbstractHarmonicMinorScale(AbstractScale): second to a leading tone. This is the only scale to use the "_alteredDegrees" property. - ''' - def __init__(self, mode=None, **keywords): + mode is not used + ''' + def __init__(self, mode: str | None = None, **keywords) -> None: super().__init__(**keywords) self.type = 'Abstract Harmonic Minor' self.octaveDuplicating = True @@ -875,9 +876,10 @@ def buildNetwork(self): class AbstractMelodicMinorScale(AbstractScale): ''' A directional scale. - ''' - def __init__(self, mode=None, **keywords): + mode is not used. + ''' + def __init__(self, mode: str | None = None, **keywords) -> None: super().__init__(**keywords) self.type = 'Abstract Melodic Minor' self.octaveDuplicating = True @@ -966,7 +968,7 @@ class AbstractRagAsawari(AbstractScale): ''' A pseudo raga-scale. ''' - def __init__(self, **keywords): + def __init__(self, **keywords) -> None: super().__init__(**keywords) self.type = 'Abstract Rag Asawari' self.octaveDuplicating = True @@ -1054,7 +1056,7 @@ class AbstractRagMarwa(AbstractScale): ''' A pseudo raga-scale. ''' - def __init__(self, **keywords): + def __init__(self, **keywords) -> None: super().__init__(**keywords) self.type = 'Abstract Rag Marwa' self.octaveDuplicating = True @@ -2557,7 +2559,7 @@ class DiatonicScale(ConcreteScale): ''' usePitchDegreeCache = True - def __init__(self, tonic=None, **keywords): + def __init__(self, tonic: str | pitch.Pitch | note.Note | None = None, **keywords): super().__init__(tonic=tonic, **keywords) self._abstract: AbstractDiatonicScale = AbstractDiatonicScale(**keywords) self.type = 'diatonic' diff --git a/music21/stream/base.py b/music21/stream/base.py index 72c4f9fd0c..26f6447d63 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -5664,7 +5664,7 @@ def getInstruments(self, * Changed in v8: recurse is True by default. ''' - instObj = None + instObj: instrument.Instrument | None = None if not recurse: sIter = self.iter() From 490d9f02cc631b6391b23ae52b215582ab3e4d8e Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Mon, 5 Dec 2022 23:52:11 -1000 Subject: [PATCH 09/16] fix big errors --- music21/midi/__init__.py | 8 +++++--- music21/midi/translate.py | 2 +- music21/romanText/tsvConverter.py | 1 - 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/music21/midi/__init__.py b/music21/midi/__init__.py index ac5af1bbdd..a12434df04 100644 --- a/music21/midi/__init__.py +++ b/music21/midi/__init__.py @@ -1159,16 +1159,18 @@ def readUntilLowByte(self, oldBytes: bytes) -> tuple[int, bytes]: >>> mt = midi.MidiTrack(1) >>> dt = midi.DeltaTime(mt) - >>> dt.read(b'\x20') + >>> dt.readUntilLowByte(b'\x20') (32, b'') - >>> dt.read(b'\x20hello') + >>> dt.readUntilLowByte(b'\x20hello') (32, b'hello') here the '\x82' is above 0x80 so the 'h' is read as part of the continuation. - >>> dt.read(b'\x82hello') + >>> dt.readUntilLowByte(b'\x82hello') (360, b'ello') + + Changed in v9: was read() but had an incompatible signature with MidiEvent ''' self.time, newBytes = getVariableLengthNumber(oldBytes) return self.time, newBytes diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 143c0a2e2d..479a6eb9bd 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -850,7 +850,7 @@ def midiEventsToInstrument( from music21 import midi as midiModule if not common.isListLike(eventList): - event = t.cast(midi.MidiEvent, eventList) + event = t.cast(midiModule.MidiEvent, eventList) else: # get the second event; first is delta time event = eventList[1] diff --git a/music21/romanText/tsvConverter.py b/music21/romanText/tsvConverter.py index 5e7ad949f3..5756f8f70e 100644 --- a/music21/romanText/tsvConverter.py +++ b/music21/romanText/tsvConverter.py @@ -24,7 +24,6 @@ from music21 import chord from music21 import common -from music21.common.types import OffsetQL from music21 import environment from music21 import harmony from music21 import key From 125ede85a5dbfcf1eed6c872432ad9e1f5e8bf62 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 6 Dec 2022 00:00:33 -1000 Subject: [PATCH 10/16] where is this coming from? --- music21/expressions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/music21/expressions.py b/music21/expressions.py index 35550ba9d7..0c2935945f 100644 --- a/music21/expressions.py +++ b/music21/expressions.py @@ -1528,7 +1528,8 @@ def __init__(self, arpeggioType: str | None = None, **keywords): arpeggioType = 'normal' if arpeggioType not in ('normal', 'up', 'down', 'non-arpeggio'): raise ValueError( - 'Arpeggio type must be "normal", "up", "down", or "non-arpeggio"' + 'Arpeggio type must be "normal", "up", "down", or "non-arpeggio", ' + + f'not {arpeggioType!r}.' ) self.type = arpeggioType @@ -1565,7 +1566,8 @@ def __init__(self, super().__init__(*spannedElements, **keywords) if arpeggioType not in ('normal', 'up', 'down', 'non-arpeggio'): raise ValueError( - 'Arpeggio type must be "normal", "up", "down", or "non-arpeggio"' + 'Arpeggio type must be "normal", "up", "down", or "non-arpeggio", ' + + f'not {arpeggioType!r}.' ) self.type = arpeggioType From ebcc5feab7517ec026d1f79567042b7873cf2223 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 6 Dec 2022 08:58:09 -1000 Subject: [PATCH 11/16] consistent None vs '' in instruments/arpegg --- music21/expressions.py | 2 +- music21/instrument.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/expressions.py b/music21/expressions.py index 0c2935945f..9b0fd292ba 100644 --- a/music21/expressions.py +++ b/music21/expressions.py @@ -1524,7 +1524,7 @@ class ArpeggioMark(Expression): ''' def __init__(self, arpeggioType: str | None = None, **keywords): super().__init__(**keywords) - if arpeggioType is None: + if not arpeggioType: arpeggioType = 'normal' if arpeggioType not in ('normal', 'up', 'down', 'non-arpeggio'): raise ValueError( diff --git a/music21/instrument.py b/music21/instrument.py index 1ece83b862..b7dec410e2 100644 --- a/music21/instrument.py +++ b/music21/instrument.py @@ -2271,7 +2271,7 @@ def partitionByInstrument(streamObj: stream.Stream) -> stream.Stream: # duration will have been set with sub.extendDuration above end = i.offset + i.duration.quarterLength # get destination Part - p = names[i.instrumentName]['Part'] + p = names[i.instrumentName or '']['Part'] coll = subStream.getElementsByOffset( start, From d7fde6b32699aff0a845d7419e3becb6216b38c4 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 6 Dec 2022 12:41:21 -1000 Subject: [PATCH 12/16] textStripValid is gone, long live strippedText --- music21/musicxml/xmlToM21.py | 355 ++++++++++++++++------------------- 1 file changed, 165 insertions(+), 190 deletions(-) diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 0cfc3c0e7b..009315f2c1 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -88,56 +88,44 @@ def _clean(badStr: str | None) -> str | None: return goodStr -# Durations - -def textNotNone(mxObj: ET.Element | None) -> bool: +def strippedText(mxObj: ET.Element | None) -> str: ''' - returns True is mxObj is not None - and mxObj.text is not None + Returns the `mxObj.text.strip()` from an Element (or None) + taking into account that `.text` might be None, or the + Element might be undefined. - >>> from xml.etree.ElementTree import Element, SubElement - >>> e = Element('an-element') - >>> musicxml.xmlToM21.textNotNone(e) - False - >>> e.text = 'hello' - >>> musicxml.xmlToM21.textNotNone(e) - True - ''' - if mxObj is None: - return False - if not hasattr(mxObj, 'text'): - return False - if mxObj.text is None: - return False - return True + Replacement for the older textStripValid() - -def textStripValid(mxObj: ET.Element | None) -> bool: - ''' - returns True if textNotNone(mxObj) - and mxObj.text.strip() is not empty - - >>> from xml.etree.ElementTree import Element, SubElement + >>> from xml.etree.ElementTree import Element >>> e = Element('an-element') - >>> musicxml.xmlToM21.textStripValid(e) - False + >>> musicxml.xmlToM21.strippedText(e) + '' >>> e.text = ' ' - >>> musicxml.xmlToM21.textStripValid(e) - False - >>> e.text = 'hello' - >>> musicxml.xmlToM21.textStripValid(e) - True + >>> musicxml.xmlToM21.strippedText(e) + '' + >>> e.text = ' hello ' + >>> musicxml.xmlToM21.strippedText(e) + 'hello' + + >>> musicxml.xmlToM21.strippedText(None) + '' + >>> musicxml.xmlToM21.strippedText(440.0) + '' + + New in v9. ''' - if not textNotNone(mxObj): - return False - if t.TYPE_CHECKING: - assert mxObj is not None and mxObj.text is not None - - if not mxObj.text.strip(): - return False - return True + if mxObj is None: + return '' + try: + txt = mxObj.text + if txt is None: + return '' + return txt.strip() + except AttributeError: + return '' +# Durations def musicXMLTypeToType(value: str) -> str: ''' Utility function to convert a MusicXML duration type to a music21 duration type. @@ -1412,10 +1400,8 @@ def processEncoding(self, encoding: ET.Element, md: metadata.Metadata) -> None: # TODO: encoding date multiple # TODO: encoding-description (string) multiple for software in encoding.findall('software'): - if textStripValid(software): - if t.TYPE_CHECKING: - assert software.text is not None - md.add('software', software.text.strip()) + if softwareText := strippedText(software): + md.add('software', softwareText) for supports in encoding.findall('supports'): # todo: element: required @@ -1519,7 +1505,10 @@ class PartParser(XMLParserBase): called out for multiprocessing potential in future ''' - def __init__(self, mxPart=None, mxScorePart=None, parent=None): + def __init__(self, + mxPart: ET.Element | None = None, + mxScorePart: ET.Element | None = None, + parent: MusicXMLImporter | None = None): super().__init__() self.mxPart = mxPart self.mxScorePart = mxScorePart @@ -1530,16 +1519,14 @@ def __init__(self, mxPart=None, mxScorePart=None, parent=None): self.partId = list(parent.mxScorePartDict.keys())[0] else: self.partId = '' - self._parent = common.wrapWeakref(parent) - if parent is not None: - self.spannerBundle = parent.spannerBundle - else: - self.spannerBundle = spanner.SpannerBundle() + self.parent = parent if parent is not None else MusicXMLImporter() + self.spannerBundle = self.parent.spannerBundle self.stream: stream.Part = stream.Part() if self.mxPart is not None: for mxStaves in self.mxPart.findall('measure/attributes/staves'): - if int(mxStaves.text) > 1: + stavesText = strippedText(mxStaves) + if stavesText and int(stavesText) > 1: self.stream = stream.PartStaff() # PartStaff inherits from Part, so okay. break @@ -1547,7 +1534,7 @@ def __init__(self, mxPart=None, mxScorePart=None, parent=None): self.staffReferenceList: list[StaffReferenceType] = [] - self.lastTimeSignature = None + self.lastTimeSignature: meter.TimeSignature | None = None self.lastMeasureWasShort = False self.lastMeasureOffset = 0.0 @@ -1558,22 +1545,18 @@ def __init__(self, mxPart=None, mxScorePart=None, parent=None): self.maxStaves = 1 # will be changed in measure parsing... self.lastMeasureNumber = 0 - self.lastNumberSuffix = None + self.lastNumberSuffix: str | None = None self.multiMeasureRestsToCapture = 0 - self.activeMultiMeasureRestSpanner = None + self.activeMultiMeasureRestSpanner: spanner.MultiMeasureRest | None = None - self.activeInstrument = None + self.activeInstrument: instrument.Instrument | None = None self.firstMeasureParsed = False # has the first measure been parsed yet? self.activeAttributes = None # divisions, clef, etc. - self.lastDivisions = defaults.divisionsPerQuarter # give a default value for testing + self.lastDivisions: int = defaults.divisionsPerQuarter # give a default value for testing self.appendToScoreAfterParse = True - self.lastMeasureParser = None - - @property - def parent(self): - return common.unwrapWeakref(self._parent) + self.lastMeasureParser: MeasureParser | None = None def parse(self): ''' @@ -1722,25 +1705,25 @@ def _adjustMidiData(mc): if mxMIDIInstrument is not None: mxMidiProgram = mxMIDIInstrument.find('midi-program') mxMidiUnpitched = mxMIDIInstrument.find('midi-unpitched') - if textStripValid(mxMidiUnpitched): + if midiUnpitchedText := strippedText(mxMidiUnpitched): pm = PercussionMapper() try: - i = pm.midiPitchToInstrument(_adjustMidiData(mxMidiUnpitched.text)) + i = pm.midiPitchToInstrument(_adjustMidiData(midiUnpitchedText)) except MIDIPercussionException as mpe: # objects not yet existing in m21 such as Cabasa warnings.warn(MusicXMLWarning(mpe)) i = instrument.UnpitchedPercussion() - i.percMapPitch = _adjustMidiData(mxMidiUnpitched.text) - elif textStripValid(mxMidiProgram): + i.percMapPitch = _adjustMidiData(midiUnpitchedText) + elif midiProgramText := strippedText(mxMidiProgram): try: - i = instrument.instrumentFromMidiProgram(_adjustMidiData(mxMidiProgram.text)) + i = instrument.instrumentFromMidiProgram(_adjustMidiData(midiProgramText)) except instrument.InstrumentException as ie: warnings.warn(MusicXMLWarning(ie)) # Invalid MIDI program, out of range 0-127 i = instrument.Instrument() seta(i, mxMIDIInstrument, 'midi-channel', transform=_adjustMidiData) if i is None: - # This catches textStripValid() returning False or no mxMIDIInstrument + # This catches no mxMIDIInstrument or empty text. i = instrument.Instrument() # for now, just get first instrument @@ -1773,15 +1756,14 @@ def _adjustMidiData(mc): @staticmethod def reclassifyInstrumentFromName( - i: instrument.Instrument, mxScoreInstrument: ET.Element) -> instrument.Instrument: + i: instrument.Instrument, + mxScoreInstrument: ET.Element, + ) -> instrument.Instrument: mxInstrumentName = mxScoreInstrument.find('instrument-name') - if mxInstrumentName is not None and textStripValid(mxInstrumentName): - if t.TYPE_CHECKING: - assert mxInstrumentName.text is not None - + if instrumentNameText := strippedText(mxInstrumentName): previous_midi_channel = i.midiChannel try: - i = instrument.fromString(mxInstrumentName.text.strip()) + i = instrument.fromString(instrumentNameText) except instrument.InstrumentException: i = instrument.Instrument() i.midiChannel = previous_midi_channel @@ -1986,7 +1968,7 @@ def xmlMeasureToMeasure(self, mxMeasure: ET.Element) -> stream.Measure: try: measureParser.parse() except MusicXMLImportException as e: - e.measureNumber = measureParser.measureNumber + e.measureNumber = str(measureParser.measureNumber) e.partName = self.stream.partName raise e except Exception as e: # pylint: disable=broad-except @@ -2015,7 +1997,16 @@ def xmlMeasureToMeasure(self, mxMeasure: ET.Element) -> stream.Measure: if measureParser.fullMeasureRest is True: # recurse is necessary because it could be in voices... r1 = m[note.Rest].first() - lastTSQl = self.lastTimeSignature.barDuration.quarterLength + + if t.TYPE_CHECKING: + # fullMeasureRest is True, means Rest will be found + assert r1 is not None + + if self.lastTimeSignature is not None: + lastTSQl = self.lastTimeSignature.barDuration.quarterLength + else: + lastTSQl = 4.0 # sensible default. + if (r1.fullMeasure is True # set by xml measure='yes' or (r1.duration.quarterLength != lastTSQl and r1.duration.type in ('whole', 'breve') @@ -2208,7 +2199,10 @@ def adjustTimeAttributesFromMeasure(self, m: stream.Measure): # warnings.warn([self.lastTimeSignature], MusicXMLWarning) # warnings.warn([self.lastTimeSignature.barDuration], MusicXMLWarning) - lastTimeSignatureQuarterLength = self.lastTimeSignature.barDuration.quarterLength + if self.lastTimeSignature is not None: + lastTimeSignatureQuarterLength = self.lastTimeSignature.barDuration.quarterLength + else: + lastTimeSignatureQuarterLength = 4.0 # sensible default. if mHighestTime >= lastTimeSignatureQuarterLength: mOffsetShift = mHighestTime @@ -2347,42 +2341,33 @@ class MeasureParser(XMLParserBase): 'bookmark': None, # Note: is handled separately... } - def __init__(self, mxMeasure=None, parent=None): + def __init__(self, + mxMeasure: ET.Element | None = None, + parent: PartParser | None = None): super().__init__() self.mxMeasure = mxMeasure - self.mxMeasureElements = [] + self.mxMeasureElements: list[ET.Element] = [] - self.parent = parent # PartParser + self.parent: PartParser = parent if parent is not None else PartParser() self.transposition = None - if parent is not None: - self.spannerBundle = parent.spannerBundle - else: - self.spannerBundle = spanner.SpannerBundle() - + self.spannerBundle = self.parent.spannerBundle self.staffReference: StaffReferenceType = {} - if parent is not None: - # list of current tuplets or Nones - self.activeTuplets: list[duration.Tuplet | None] = parent.activeTuplets - else: - self.activeTuplets: list[duration.Tuplet | None] = [None] * 7 + self.activeTuplets: list[duration.Tuplet | None] = self.parent.activeTuplets self.useVoices = False - self.voicesById = {} - self.voiceIndices = set() + self.voicesById: dict[str | int, stream.Voice] = {} + self.voiceIndices: set[str | int] = set() self.staves = 1 self.activeAttributes = None self.attributesAreInternal = True - self.measureNumber = None - self.numberSuffix = None + self.measureNumber = 0 + self.numberSuffix = '' - if parent is not None: - self.divisions = parent.lastDivisions - else: - self.divisions = defaults.divisionsPerQuarter + self.divisions = self.parent.lastDivisions # key is a tuple of the # staff number (or None) and offsetMeasureNote, and the value is a @@ -2390,9 +2375,9 @@ def __init__(self, mxMeasure=None, parent=None): self.staffLayoutObjects: dict[tuple[int | None, float], layout.StaffLayout] = {} self.stream = stream.Measure() - self.mxNoteList = [] # for accumulating notes in chords - self.mxLyricList = [] # for accumulating lyrics assigned to chords - self.nLast = None # for adding notes to spanners. + self.mxNoteList: list[ET.Element] = [] # for accumulating notes in chords + self.mxLyricList: list[ET.Element] = [] # for accumulating lyrics assigned to chords + self.nLast: note.GeneralNote | None = None # for adding notes to spanners. # Sibelius 7.1 only puts a tag on the # first note of a chord, and MuseScore doesn't put one @@ -2400,18 +2385,13 @@ def __init__(self, mxMeasure=None, parent=None): # that we keep track of the last voice. # there is an effort to translate the voice text to an int, but if that fails (unlikely) # we store whatever we find - self.lastVoice = None + self.lastVoice: str | int | None = None self.fullMeasureRest = False # for keeping track of full-measureRests. self.restAndNoteCount = {'rest': 0, 'note': 0} - if parent is not None: - # share dict - self.lastClefs: dict[int, clef.Clef | None] = self.parent.lastClefs - else: - # a dict of clefs for staffIndexes: - self.lastClefs: dict[int, clef.Clef | None] = {NO_STAFF_ASSIGNED: None} + self.lastClefs: dict[int, clef.Clef | None] = self.parent.lastClefs self.parseIndex = 0 # what is the offset in the measure of the current note position? @@ -2622,12 +2602,9 @@ def xmlBackup(self, mxObj: ET.Element): 0.0 ''' mxDuration = mxObj.find('duration') - if mxDuration is not None and textStripValid(mxDuration): - if t.TYPE_CHECKING: - assert mxDuration.text is not None - + if durationText := strippedText(mxDuration): change = common.numberTools.opFrac( - float(mxDuration.text.strip()) / self.divisions + float(durationText) / self.divisions ) self.offsetMeasureNote -= change # check for negative offsets produced by @@ -2640,11 +2617,9 @@ def xmlForward(self, mxObj: ET.Element): Parse a forward tag by changing :attr:`offsetMeasureNote`. ''' mxDuration = mxObj.find('duration') - if mxDuration is not None and textStripValid(mxDuration): - if t.TYPE_CHECKING: - assert mxDuration.text is not None + if durationText := strippedText(mxDuration): change = common.numberTools.opFrac( - float(mxDuration.text.strip()) / self.divisions + float(durationText) / self.divisions ) # Create hidden rest (in other words, a spacer) @@ -2713,11 +2688,12 @@ def hasSystemLayout(): if mxMeasureNumbering is not None: # TODO: musicxml 4: system="yes/no" -- does this apply to whole system? # TODO: musicxml 4: staff attribute. - m.style.measureNumbering = mxMeasureNumbering.text + m_style = t.cast(style.StreamStyle, m.style) + m_style.measureNumbering = mxMeasureNumbering.text st = style.TextStyle() self.setPrintStyleAlign(mxMeasureNumbering, st) # TODO: musicxml 4: multiple-rest-always, multiple-rest-range - m.style.measureNumberingStyle = st + m_style.measureNumberStyle = st # TODO: part-name-display # TODO: part-abbreviation display # TODO: print-attributes: staff-spacing, blank-page; skip deprecated staff-spacing @@ -3182,15 +3158,15 @@ def xmlToPitch(self, mxNote, inputM21=None): seta(p, mxPitch, 'octave', transform=int) mxAlter = mxPitch.find('alter') accAlter = None - if textStripValid(mxAlter): - accAlter = float(mxAlter.text.strip()) + if alterText := strippedText(mxAlter): + accAlter = float(alterText) mxAccidental = mxNote.find('accidental') mxAccidentalName = None - if textStripValid(mxAccidental): + if accidentalText := strippedText(mxAccidental): # MuseScore 0.9 made empty accidental tags for notes that did not # need an accidental display. - mxAccidentalName = mxAccidental.text.strip() + mxAccidentalName = accidentalText if mxAccidentalName is not None: try: @@ -3215,7 +3191,11 @@ def xmlToPitch(self, mxNote, inputM21=None): return p - def xmlToUnpitched(self, mxUnpitched, inputM21=None) -> note.Unpitched: + def xmlToUnpitched( + self, + mxUnpitched: ET.Element, + inputM21: note.Unpitched | None = None, + ) -> note.Unpitched: ''' Set `displayStep` and `displayOctave` from `mxUnpitched`. @@ -3244,30 +3224,39 @@ def xmlToUnpitched(self, mxUnpitched, inputM21=None) -> note.Unpitched: mxDisplayStep = mxUnpitched.find('display-step') mxDisplayOctave = mxUnpitched.find('display-octave') - if textStripValid(mxDisplayStep): - unp.displayStep = mxDisplayStep.text.strip() - if textStripValid(mxDisplayOctave): - unp.displayOctave = int(mxDisplayOctave.text.strip()) + if displayStepText := strippedText(mxDisplayStep): + unp.displayStep = displayStepText # type: ignore # str vs literal CDEFGAB + if displayOctaveText := strippedText(mxDisplayOctave): + unp.displayOctave = int(displayOctaveText) return unp - def xmlToAccidental(self, mxAccidental, inputM21=None): + def xmlToAccidental( + self, + mxAccidental: ET.Element, + inputM21: pitch.Accidental | None = None, + ) -> pitch.Accidental: ''' >>> from xml.etree.ElementTree import fromstring as EL >>> MP = musicxml.xmlToM21.MeasureParser() + >>> a = EL('sharp') + >>> b = MP.xmlToAccidental(a) + >>> b.name + 'sharp' + >>> b.alter + 1.0 + >>> b.displayStyle + 'parentheses' + >>> a = EL('half-flat') >>> b = pitch.Accidental() - >>> MP.xmlToAccidental(a, b) + >>> unused = MP.xmlToAccidental(a, b) >>> b.name 'half-flat' >>> b.alter -0.5 - >>> a = EL('sharp') - >>> b = MP.xmlToAccidental(a) - >>> b.displayStyle - 'parentheses' >>> a = EL('sharp') >>> b = MP.xmlToAccidental(a) @@ -3285,7 +3274,7 @@ def xmlToAccidental(self, mxAccidental, inputM21=None): acc = inputM21 try: - mxName = mxAccidental.text.strip().lower() + mxName = strippedText(mxAccidental).lower() except AttributeError: return acc @@ -3312,8 +3301,7 @@ def xmlToAccidental(self, mxAccidental, inputM21=None): # TODO: attr: cautionary self.setEditorial(mxAccidental, acc) - if inputM21 is None: - return acc + return acc def xmlToRest(self, mxRest): # noinspection PyShadowingNames @@ -3380,11 +3368,10 @@ def xmlToRest(self, mxRest): self.parent.applyMultiMeasureRest(r) ds = mxRestTag.find('display-step') - if textStripValid(ds): - ds_text = ds.text.strip() + if ds_text := strippedText(ds): do = mxRestTag.find('display-octave') - if textStripValid(do): - ds_text += do.text.strip() + if do_text := strippedText(do): + ds_text += do_text.strip() tempP = pitch.Pitch(ds_text) # musicxml records rest display as a pitch in the current @@ -3573,8 +3560,7 @@ def xmlToDuration(self, mxNote, inputM21=None): qLen = 0.0 mxType = mxNote.find('type') - if textStripValid(mxType): - typeStr = mxType.text.strip() + if typeStr := strippedText(mxType): durationType = musicXMLTypeToType(typeStr) forceRaw = False @@ -3718,8 +3704,8 @@ def flatten(mx, name): fermataType = mxObj.get('type') if fermataType is not None: fermata.type = fermataType - if textStripValid(mxObj): - fermata.shape = mxObj.text.strip() # type: ignore + if notationText := strippedText(mxObj): + fermata.shape = notationText n.expressions.append(fermata) # get any arpeggios, store in expressions. @@ -3816,13 +3802,13 @@ def xmlTechnicalToArticulation(self, mxObj): _synchronizeIds(mxObj, tech) if tag == 'fingering': self.handleFingering(tech, mxObj) - if tag in ('handbell', 'other-technical') and textStripValid(mxObj): + if tag in ('handbell', 'other-technical') and strippedText(mxObj): # The handbell element represents notation for various # techniques used in handbell and handchime music. Valid # values are belltree [v3.1], damp, echo, gyro, hand martellato, # mallet lift, mallet table, martellato, martellato lift, # muted martellato, pluck lift, and swing. - tech.displayText = mxObj.text + tech.displayText = strippedText(mxObj) if tag in ('fret', 'string'): try: tech.number = int(mxObj.text) @@ -3933,12 +3919,12 @@ def xmlToArticulation(self, mxObj): pointDirection = mxObj.get('type') if pointDirection is not None: articulationObj.pointDirection = pointDirection - if tag in ('doit', 'falloff', 'plop', 'scoop'): + elif tag in ('doit', 'falloff', 'plop', 'scoop'): self.setLineStyle(mxObj, articulationObj) - if tag == 'breath-mark' and textStripValid(mxObj): - articulationObj.symbol = mxObj.text - if tag == 'other-articulation' and textStripValid(mxObj): - articulationObj.displayText = mxObj.text + elif tag == 'breath-mark' and (breathText := strippedText(mxObj)): + articulationObj.symbol = breathText + elif tag == 'other-articulation' and (otherText := strippedText(mxObj)): + articulationObj.displayText = otherText return articulationObj else: @@ -4364,10 +4350,10 @@ def xmlToTuplets(self, mxNote: ET.Element) -> list[duration.Tuplet]: mxNormalType = mxTimeModification.find('normal-type') musicXMLNormalType: str - if textStripValid(mxNormalType): - musicXMLNormalType = mxNormalType.text.strip() # type: ignore + if normalTypeText := strippedText(mxNormalType): + musicXMLNormalType = normalTypeText else: - musicXMLNormalType = mxNote.find('type').text.strip() # type: ignore + musicXMLNormalType = strippedText(mxNote.find('type')) durationNormalType = musicXMLTypeToType(musicXMLNormalType) numDots = len(mxTimeModification.findall('normal-dot')) @@ -4685,26 +4671,30 @@ def insertInMeasureOrVoice(self, mxElement, el): insertStream = thisVoice insertStream.coreInsert(self.offsetMeasureNote, el) - def findM21VoiceFromXmlVoice(self, mxVoice=None): + def findM21VoiceFromXmlVoice( + self, + mxVoice: ET.Element | None = None, + ) -> stream.Voice | None: ''' Find the stream.Voice object from a tag or None. ''' m = self.stream - if not textStripValid(mxVoice): + useVoice: str | int | None + if strippedText(mxVoice): + useVoice = strippedText(mxVoice) + try: + self.lastVoice = int(useVoice) + except ValueError: + self.lastVoice = useVoice + else: useVoice = self.lastVoice if useVoice is None: # pragma: no cover warnings.warn('Cannot put in an element with a missing voice tag when ' + 'no previous voice tag was given. Assuming voice 1... ', MusicXMLWarning) useVoice = 1 - else: - useVoice = mxVoice.text.strip() - try: - self.lastVoice = int(useVoice) - except ValueError: - self.lastVoice = useVoice - thisVoice = None + thisVoice: stream.Voice | None = None if useVoice in self.voicesById: thisVoice = self.voicesById[useVoice] elif int(useVoice) in self.voicesById: @@ -4980,13 +4970,8 @@ def xmlToChordSymbol( chordKindStr: str = '' mxKind = mxHarmony.find('kind') - if textStripValid(mxKind): - if t.TYPE_CHECKING: - assert mxKind is not None - mxKindText = mxKind.text - if t.TYPE_CHECKING: - assert mxKindText is not None - chordKind = mxKindText.strip() + if mxKindText := strippedText(mxKind): + chordKind = mxKindText mxFrame = mxHarmony.find('frame') @@ -5006,13 +4991,11 @@ def xmlToChordSymbol( # TODO: musicxml 4: bass-separator: use something besides slash on output. mxInversion = mxHarmony.find('inversion') - if textStripValid(mxInversion): + if inversionText := strippedText(mxInversion): # TODO: print-style for inversion # TODO: musicxml 4: text attribute overrides display of the inversion. - if t.TYPE_CHECKING: - assert mxInversion is not None and mxInversion.text is not None + inversion = int(inversionText) - inversion = int(mxInversion.text.strip()) # TODO: print-style if chordKind: # two ways of doing it... @@ -5021,10 +5004,9 @@ def xmlToChordSymbol( # Get m21 chord kind from dict of musicxml aliases ("dominant" -> "dominant-seventh") if chordKind in harmony.CHORD_ALIASES: chordKind = harmony.CHORD_ALIASES[chordKind] - mxKindText = mxKind.get('text') # attribute - if mxKindText is not None: - if not (mxKindText == '' and chordKind != 'none'): - chordKindStr = mxKindText + mxKindText = mxKind.get('text') or '' # attribute + if not (mxKindText == '' and chordKind != 'none'): + chordKindStr = mxKindText # TODO: root vs. function; see group "harmony-chord") mxRoot = mxHarmony.find('root') @@ -5241,10 +5223,7 @@ def xmlToTextExpression(self, mxWords): # environLocal.printDebug(['mxToTextExpression()', mxWords, mxWords.charData]) # content can be passed with creation argument - if textStripValid(mxWords): - wordText = mxWords.text.strip() - else: - wordText = '' + wordText = strippedText(mxWords) te = expressions.TextExpression(wordText) self.setTextFormatting(mxWords, te) return te @@ -5253,10 +5232,7 @@ def xmlToRehearsalMark(self, mxRehearsal): ''' Return a rehearsal mark from a rehearsal tag. ''' - if textStripValid(mxRehearsal): - rehearsalText = mxRehearsal.text.strip() - else: - rehearsalText = '' + rehearsalText = strippedText(mxRehearsal) rm = expressions.RehearsalMark(rehearsalText) self.setTextFormatting(mxRehearsal, rm) return rm @@ -6136,8 +6112,7 @@ def updateVoiceInformation(self): mxm = self.mxMeasure for mxn in mxm.findall('note'): voice = mxn.find('voice') - if textStripValid(voice): - vIndex = voice.text.strip() + if vIndex := strippedText(voice): self.voiceIndices.add(vIndex) # it is a set, so no need to check if already there # additional time < 1 sec per ten million ops. From 343a107609816c69cc10fded2f098b39aebc6a23 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 6 Dec 2022 14:50:22 -1000 Subject: [PATCH 13/16] all annotation-unchecked warnings done --- music21/abcFormat/__init__.py | 45 +++++++++++++++++------------- music21/analysis/discrete.py | 15 +++++----- music21/articulations.py | 2 +- music21/bar.py | 2 +- music21/capella/fromCapellaXML.py | 5 ++-- music21/clef.py | 4 +-- music21/common/classTools.py | 2 +- music21/common/weakrefTools.py | 2 +- music21/converter/__init__.py | 2 +- music21/converter/subConverters.py | 2 +- music21/corpus/corpora.py | 9 +++--- music21/derivation.py | 7 +++-- music21/features/native.py | 5 +++- music21/figuredBass/realizer.py | 38 ++++++++++++++----------- music21/harmony.py | 2 +- music21/humdrum/spineParser.py | 36 ++++++++++++++---------- music21/metadata/primitives.py | 4 +-- music21/musicxml/xmlToM21.py | 14 ++++++++-- 18 files changed, 112 insertions(+), 84 deletions(-) diff --git a/music21/abcFormat/__init__.py b/music21/abcFormat/__init__.py index 8c8b4d45fb..7b0f552064 100644 --- a/music21/abcFormat/__init__.py +++ b/music21/abcFormat/__init__.py @@ -162,7 +162,7 @@ class ABCToken(prebase.ProtoM21Object, common.objects.EqualSlottedObjectMixin): ''' __slots__ = ('src',) - def __init__(self, src=''): + def __init__(self, src: str = ''): self.src: str = src # store source character sequence def _reprInternal(self): @@ -237,7 +237,7 @@ class ABCMetadata(ABCToken): # given a logical unit, create an object # may be a chord, notes, metadata, bars - def __init__(self, src=''): + def __init__(self, src: str = ''): super().__init__(src) self.tag: str = '' self.data: str = '' @@ -803,7 +803,7 @@ def __init__(self, src): self.barStyle = '' # regular, heavy-light, etc self.repeatForm = '' # end, start, bidrectional, first, second - def parse(self): + def parse(self) -> None: ''' Assign the bar-type based on the source string. @@ -1154,7 +1154,7 @@ class ABCSlurStart(ABCToken): ''' __slots__ = ('slurObj',) - def __init__(self, src=''): + def __init__(self, src: str = ''): super().__init__(src) self.slurObj: spanner.Slur | None = None @@ -1183,11 +1183,11 @@ class ABCCrescStart(ABCToken): ''' __slots__ = ('crescObj',) - def __init__(self, src=''): + def __init__(self, src: str = ''): super().__init__(src) self.crescObj: dynamics.Crescendo | None = None - def fillCresc(self): + def fillCresc(self) -> None: from music21 import dynamics self.crescObj = dynamics.Crescendo() @@ -1199,7 +1199,7 @@ class ABCDimStart(ABCToken): ''' __slots__ = ('dimObj',) - def __init__(self, src=''): + def __init__(self, src: str = ''): super().__init__(src) self.dimObj: dynamics.Diminuendo | None = None @@ -1278,9 +1278,9 @@ class ABCBrokenRhythmMarker(ABCToken): ''' __slots__ = ('data',) - def __init__(self, src=''): + def __init__(self, src: str = ''): super().__init__(src) - self.data: str | None = None + self.data: str = '' def preParse(self): ''' @@ -1328,26 +1328,26 @@ def __init__(self, src='', carriedAccidental: str = ''): self.chordSymbols: list[str] = [] # context attributes - self.inBar = None - self.inBeam = None - self.inGrace = None + self.inBar: bool | None = None + self.inBeam: bool | None = None + self.inGrace: bool | None = None # provide default duration from handler; may change during piece self.activeDefaultQuarterLength: float | None = None # store if a broken symbol applies; a pair of symbols, position (left, right) - self.brokenRhythmMarker = None + self.brokenRhythmMarker: tuple[str, str] | None = None # store key signature for pitch processing; this is an M21Object - self.activeKeySignature = None + self.activeKeySignature: key.KeySignature | None = None # store a tuplet if active - self.activeTuplet = None + self.activeTuplet: duration.Tuplet | None = None # store a spanner if active self.applicableSpanners: list[spanner.Spanner] = [] - # store a tie if active - self.tie = None + # store a tie type if active + self.tie: str | None = None # store articulations if active self.articulations: list[str] = [] @@ -2501,7 +2501,7 @@ def tokenize(self, strSrc: str) -> None: # no action: normal continuation of 1 char pass - def tokenProcess(self): + def tokenProcess(self) -> None: ''' Process all token objects. First, calls preParse(), then does context assignments, then calls parse(). @@ -2582,6 +2582,9 @@ def tokenProcess(self): # notes within slur marks need to be added to the spanner if isinstance(token, ABCSlurStart): token.fillSlur() + if t.TYPE_CHECKING: + assert token.slurObj is not None + self.activeSpanners.append(token.slurObj) self.activeParens.append('Slur') elif isinstance(token, ABCParenStop): @@ -2618,11 +2621,15 @@ def tokenProcess(self): if isinstance(token, ABCCrescStart): token.fillCresc() + if t.TYPE_CHECKING: + assert token.crescObj is not None self.activeSpanners.append(token.crescObj) self.activeParens.append('Crescendo') if isinstance(token, ABCDimStart): token.fillDim() + if t.TYPE_CHECKING: + assert token.dimObj is not None self.activeSpanners.append(token.dimObj) self.activeParens.append('Diminuendo') @@ -3247,7 +3254,7 @@ class ABCHandlerBar(ABCHandler): # divide elements of a character stream into objects and handle # store in a list, and pass global information to components - def __init__(self): + def __init__(self) -> None: # tokens are ABC objects in a linear stream super().__init__() diff --git a/music21/analysis/discrete.py b/music21/analysis/discrete.py index 357abe98ba..94b296bbd6 100644 --- a/music21/analysis/discrete.py +++ b/music21/analysis/discrete.py @@ -37,11 +37,9 @@ from music21 import key from music21 import pitch - if t.TYPE_CHECKING: from music21 import stream - environLocal = environment.Environment('analysis.discrete') @@ -392,7 +390,7 @@ def _convoluteDistribution(self, pcDistribution, weightType='major'): solution[i] += (toneWeights[(j - i) % 12] * pcDistribution[j]) return solution - def _getLikelyKeys(self, keyResults, differences): + def _getLikelyKeys(self, keyResults, differences) -> list[t.Any] | None: ''' Takes in a list of probable key results in points and returns a list of keys in letters, sorted from most likely to least likely. ''' @@ -410,9 +408,10 @@ def _getLikelyKeys(self, keyResults, differences): # environLocal.printDebug(['added likely key', likelyKeys[pc]]) return likelyKeys - def _getDifference(self, keyResults, pcDistribution, weightType): - ''' Takes in a list of numerical probable key results and returns the - difference of the top two keys + def _getDifference(self, keyResults, pcDistribution, weightType) -> None | list[int | float]: + ''' + Takes in a list of numerical probable key results and returns the + difference of the top two keys. ''' # case of empty analysis if keyResults is None: @@ -953,14 +952,14 @@ class Ambitus(DiscreteAnalysis): # provide possible string matches for this processor identifiers = ['ambitus', 'span'] - def __init__(self, referenceStream=None): + def __init__(self, referenceStream: stream.Stream | None = None): super().__init__(referenceStream=referenceStream) # Store the min and max Pitch instances for referenceStream # set by getPitchSpan(), which is called by _generateColors() self.minPitchObj: pitch.Pitch | None = None self.maxPitchObj: pitch.Pitch | None = None - self._pitchSpanColors = OrderedDict() + self._pitchSpanColors: OrderedDict[int, str] = OrderedDict() self._generateColors() def _generateColors(self, numColors=None): diff --git a/music21/articulations.py b/music21/articulations.py index 38eca48635..65ce608019 100644 --- a/music21/articulations.py +++ b/music21/articulations.py @@ -140,7 +140,7 @@ class Articulation(base.Music21Object): ''' _styleClass: type[style.Style] = style.TextStyle - def __init__(self, **keywords): + def __init__(self, **keywords) -> None: super().__init__(**keywords) self.placement = None # declare a unit interval shift for the performance of this articulation diff --git a/music21/bar.py b/music21/bar.py index 1d5f742441..3b0eba6f03 100644 --- a/music21/bar.py +++ b/music21/bar.py @@ -266,7 +266,7 @@ class Repeat(repeat.RepeatMark, Barline): {4.0} ''' # _repeatDots = None # not sure what this is for; inherited from old modules - def __init__(self, direction='start', times=None, **keywords): + def __init__(self, direction: str = 'start', times: int | None = None, **keywords): repeat.RepeatMark.__init__(self) if direction == 'start': barType = 'heavy-light' diff --git a/music21/capella/fromCapellaXML.py b/music21/capella/fromCapellaXML.py index ad0ff6f9a3..e1b28eed8b 100644 --- a/music21/capella/fromCapellaXML.py +++ b/music21/capella/fromCapellaXML.py @@ -20,6 +20,7 @@ from __future__ import annotations from io import StringIO +import typing as t import unittest import xml.etree.ElementTree import zipfile @@ -165,7 +166,7 @@ def domElementFromText(self, xmlText=None): ''' return xml.etree.ElementTree.fromstring(xmlText) - def partScoreFromSystemScore(self, systemScore): + def partScoreFromSystemScore(self, systemScore: stream.Score) -> stream.Score: ''' Take a :class:`~music21.stream.Score` object which is organized by Systems and return a new `Score` object which is organized by @@ -174,7 +175,7 @@ def partScoreFromSystemScore(self, systemScore): # this line is redundant currently, since all we have in systemScore # are Systems, but later there will be other things. systemStream = systemScore.getElementsByClass(layout.System) - partDictById = {} + partDictById: dict[str | int, dict[str, t.Any]] = {} for thisSystem in systemStream: # this line is redundant currently, since all we have in # thisSystem are Parts, but later there will be other things. diff --git a/music21/clef.py b/music21/clef.py index 39fa27133a..4566dfe10b 100644 --- a/music21/clef.py +++ b/music21/clef.py @@ -126,7 +126,7 @@ class Clef(base.Music21Object): _styleClass = style.TextStyle classSortOrder = 0 - def __init__(self, **keywords): + def __init__(self, **keywords) -> None: super().__init__(**keywords) self.sign: str | None = None # line counts start from the bottom up, the reverse of musedata @@ -283,7 +283,7 @@ class PitchClef(Clef): ''', } - def __init__(self, **keywords): + def __init__(self, **keywords) -> None: super().__init__(**keywords) self.lowestLine: int = 31 diff --git a/music21/common/classTools.py b/music21/common/classTools.py index 865b8dcdb0..f2c4434be5 100644 --- a/music21/common/classTools.py +++ b/music21/common/classTools.py @@ -294,7 +294,7 @@ def tempAttribute(obj, attribute: str, new_val=TEMP_ATTRIBUTE_SENTINEL): setattr(obj, attribute, tempStorage) @contextlib.contextmanager -def saveAttributes(obj, *attributeList): +def saveAttributes(obj, *attributeList: str) -> t.Generator[None, None, None]: ''' Save a number of attributes in an object and then restore them afterwards. diff --git a/music21/common/weakrefTools.py b/music21/common/weakrefTools.py index e1eb0d6c2f..b37c61fbc1 100644 --- a/music21/common/weakrefTools.py +++ b/music21/common/weakrefTools.py @@ -51,7 +51,7 @@ def wrapWeakref(referent: _T) -> weakref.ReferenceType | _T: return referent -def unwrapWeakref(referent: weakref.ReferenceType) -> t.Any: +def unwrapWeakref(referent: weakref.ReferenceType | t.Any) -> t.Any: ''' Utility function that gets an object that might be an object itself or a weak reference to an object. It returns obj() if it's a weakref. diff --git a/music21/converter/__init__.py b/music21/converter/__init__.py index d09da9d2f0..ecab138362 100644 --- a/music21/converter/__init__.py +++ b/music21/converter/__init__.py @@ -493,7 +493,7 @@ class Converter: ''', } - def __init__(self): + def __init__(self) -> None: self.subConverter: subConverters.SubConverter | None = None # a stream object unthawed self._thawedStream: stream.Score | stream.Part | stream.Opus | None = None diff --git a/music21/converter/subConverters.py b/music21/converter/subConverters.py index f978b03fda..5f9f181376 100644 --- a/music21/converter/subConverters.py +++ b/music21/converter/subConverters.py @@ -87,7 +87,7 @@ class SubConverter: codecWrite: bool = False stringEncoding: str = 'utf-8' - def __init__(self, **keywords): + def __init__(self, **keywords) -> None: self._stream: stream.Score | stream.Part | stream.Opus = stream.Score() self.keywords: dict[str, t.Any] = keywords diff --git a/music21/corpus/corpora.py b/music21/corpus/corpora.py index 12b6591570..b677f5e164 100644 --- a/music21/corpus/corpora.py +++ b/music21/corpus/corpora.py @@ -710,9 +710,8 @@ class LocalCorpus(Corpus): Traceback (most recent call last): music21.exceptions21.CorpusException: The name 'core' is reserved. ''' - # CLASS VARIABLES # - _temporaryLocalPaths: dict[str, set] = {} + _temporaryLocalPaths: dict[str, set[pathlib.Path]] = {} parseUsingCorpus: bool = False # INITIALIZER # @@ -867,7 +866,7 @@ def getPaths( return Corpus._pathsCache[cacheKey] - def removePath(self, directoryPath): + def removePath(self, directoryPath: str | pathlib.Path) -> None: r''' Remove a directory path from a local corpus. @@ -885,8 +884,8 @@ def removePath(self, directoryPath): TODO: test for corpus persisted to disk without actually reindexing files on user's Desktop. ''' - temporaryPaths = LocalCorpus._temporaryLocalPaths.get( - self.name, []) + temporaryPaths: set[pathlib.Path] = LocalCorpus._temporaryLocalPaths.get( + self.name, set()) directoryPathObj: pathlib.Path = common.cleanpath(directoryPath, returnPathlib=True) if directoryPathObj in temporaryPaths: temporaryPaths.remove(directoryPathObj) diff --git a/music21/derivation.py b/music21/derivation.py index 1306cfc02e..a6685239ac 100644 --- a/music21/derivation.py +++ b/music21/derivation.py @@ -17,6 +17,7 @@ ''' from __future__ import annotations +import weakref from collections.abc import Generator import functools import typing as t @@ -148,9 +149,9 @@ class Derivation(SlottedObjectMixin): # INITIALIZER # - def __init__(self, client=None): + def __init__(self, client: base.Music21Object | None = None): # store a reference to the Music21Object that has this Derivation object as a property - self._client = None + self._client: weakref.ReferenceType | None = None self._clientId: int | None = None # store python-id to optimize w/o unwrapping self._method: str | None = None # origin should be stored as a weak ref -- the place where the client was derived from. @@ -211,7 +212,7 @@ def client(self, client: base.Music21Object | None): self._client = None else: self._clientId = id(client) - self._client = common.wrapWeakref(client) + self._client = common.wrapWeakref(client) # type: ignore def chain(self) -> Generator[base.Music21Object, None, None]: ''' diff --git a/music21/features/native.py b/music21/features/native.py index 39203d7f56..5b80a65a45 100644 --- a/music21/features/native.py +++ b/music21/features/native.py @@ -106,10 +106,13 @@ def __init__(self, dataOrStream=None, **keywords): self.isSequential = True self.dimensions = 1 - def process(self): + def process(self) -> None: ''' Do processing necessary, storing result in feature. ''' + if self.data is None or self.feature is None: # pragma: no cover + raise ValueError('Cannot process without a data instance or feature.') + allKeys = self.data['flat.getElementsByClass(Key)'] keyFeature: int | None = None if len(allKeys) == 1: diff --git a/music21/figuredBass/realizer.py b/music21/figuredBass/realizer.py index a0aeef8bf4..bd7d7fef17 100644 --- a/music21/figuredBass/realizer.py +++ b/music21/figuredBass/realizer.py @@ -45,6 +45,7 @@ import collections import copy import random +import typing as t import unittest from music21 import chord @@ -61,7 +62,11 @@ from music21.figuredBass import rules from music21.figuredBass import segment -def figuredBassFromStream(streamPart): +if t.TYPE_CHECKING: + from music21.stream.iterator import StreamIterator + + +def figuredBassFromStream(streamPart: stream.Stream) -> FiguredBassLine: # noinspection PyShadowingNames ''' Takes a :class:`~music21.stream.Part` (or another :class:`~music21.stream.Stream` subclass) @@ -89,28 +94,27 @@ def figuredBassFromStream(streamPart): * Changed in v7.3: multiple figures in same lyric (e.g. '64') now supported. ''' sf = streamPart.flatten() - sfn = sf.notes - - keyList = sf.getElementsByClass(key.Key) - myKey = None - if not keyList: - keyList = sf.getElementsByClass(key.KeySignature) - if not keyList: - myKey = key.Key('C') - else: - myKey = keyList[0].asKey('major') + sfn = sf.getElementsByClass(note.Note) + myKey: key.Key + if firstKey := sf[key.Key].first(): + myKey = firstKey + elif firstKeySignature := sf[key.KeySignature].first(): + myKey = firstKeySignature.asKey('major') else: - myKey = keyList[0] + myKey = key.Key('C') - tsList = sf.getElementsByClass(meter.TimeSignature) - if not tsList: - ts = meter.TimeSignature('4/4') + ts: meter.TimeSignature + if first_ts := sf[meter.TimeSignature].first(): + ts = first_ts else: - ts = tsList[0] + ts = meter.TimeSignature('4/4') fb = FiguredBassLine(myKey, ts) if streamPart.hasMeasures(): - paddingLeft = streamPart.measure(0).paddingLeft + m_first = streamPart.measure(0, indicesNotNumbers=True) + if t.TYPE_CHECKING: + assert m_first is not None + paddingLeft = m_first.paddingLeft if paddingLeft != 0.0: fb._paddingLeft = paddingLeft diff --git a/music21/harmony.py b/music21/harmony.py index 5a2f5a5edf..1ae6becfdb 100644 --- a/music21/harmony.py +++ b/music21/harmony.py @@ -1898,7 +1898,7 @@ def _notationString(self): return notationString - def _parseFigure(self): + def _parseFigure(self) -> None: ''' Translate the figure string (regular expression) into a meaningful Harmony object by identifying the root, bass, inversion, kind, and diff --git a/music21/humdrum/spineParser.py b/music21/humdrum/spineParser.py index adf8d80630..dde3f5fa05 100644 --- a/music21/humdrum/spineParser.py +++ b/music21/humdrum/spineParser.py @@ -48,6 +48,7 @@ import copy import math import re +import typing as t import unittest from music21 import articulations @@ -76,6 +77,9 @@ from music21.humdrum import harmparser from music21.humdrum import instruments +if t.TYPE_CHECKING: + import pathlib + environLocal = environment.Environment('humdrum.spineParser') flavors = {'JRP': False} @@ -786,11 +790,11 @@ class HumdrumFile(HumdrumDataCollection): as a mandatory argument a filename to be opened and read. ''' - def __init__(self, filename=None): + def __init__(self, filename: str | pathlib.Path | None = None): super().__init__() self.filename = filename - def parseFilename(self, filename=None): + def parseFilename(self, filename: str | pathlib.Path | None = None): if filename is None: filename = self.filename if filename is None: @@ -855,7 +859,7 @@ class SpineLine(HumdrumLine): isSpineLine = True numSpines = 0 - def __init__(self, position=0, contents=''): + def __init__(self, position: int = 0, contents: str = ''): self.position = position contents = contents.rstrip() returnList = re.split('\t+', contents) @@ -910,7 +914,7 @@ class GlobalReferenceLine(HumdrumLine): isSpineLine = False numSpines = 0 - def __init__(self, position=0, contents='!!! NUL: None'): + def __init__(self, position: int = 0, contents: str = '!!! NUL: None'): self.position = position noExclaim = re.sub(r'^!!!+', '', contents) try: @@ -961,7 +965,7 @@ class GlobalCommentLine(HumdrumLine): isSpineLine = False numSpines = 0 - def __init__(self, position=0, contents=''): + def __init__(self, position: int = 0, contents: str = ''): self.position = position value = re.sub(r'^!!+\s?', '', contents) self.contents = contents @@ -1564,24 +1568,23 @@ class SpineEvent(prebase.ProtoM21Object): >>> n ''' - def __init__(self, contents=None, position=0): - self.contents = contents - self.position = position + def __init__(self, contents: str = '', position: int = 0): + self.contents: str = contents + self.position: int = position self.protoSpineId: int = 0 self.spineId: int | None = None def _reprInternal(self): - return str(self.contents) + return self.contents def __str__(self): - return str(self.contents) + return self.contents def toNote(self, convertString=None): r''' parse the object as a \*\*kern note and return a :class:`~music21.note.Note` object (or Rest, or Chord) - >>> se = humdrum.spineParser.SpineEvent('DD#4') >>> n = se.toNote() >>> n @@ -1983,7 +1986,7 @@ def moveDynamicsAndLyricsToStreams(self): lyric = prioritiesToSearch[el.priority].contents el.lyric = lyric - def makeVoices(self): + def makeVoices(self) -> None: ''' make voices for each kernSpine -- why not just run stream.makeVoices() ? because we have more information @@ -2018,9 +2021,12 @@ def makeVoices(self): voiceNumber = int(voiceName[5]) voicePart = voices[voiceNumber] if voicePart is None: - voices[voiceNumber] = stream.Voice() - voicePart = voices[voiceNumber] + voicePart = stream.Voice() + voices[voiceNumber] = voicePart voicePart.groups.append(voiceName) + if t.TYPE_CHECKING: + assert voicePart is not None + mElOffset = mEl.offset el.remove(mEl) voicePart.coreInsert(mElOffset - lowestVoiceOffset, mEl) @@ -2833,7 +2839,7 @@ def __init__(self, codeOrAll='', valueOrNone=None, **keywords): 'RWB': '' # a warning about the representation } - def updateMetadata(self, md): + def updateMetadata(self, md: metadata.Metadata): ''' update a metadata object according to information in this GlobalReference diff --git a/music21/metadata/primitives.py b/music21/metadata/primitives.py index 1ede8eefb3..529d323da0 100644 --- a/music21/metadata/primitives.py +++ b/music21/metadata/primitives.py @@ -473,9 +473,9 @@ class DatePrimitive(prebase.ProtoM21Object): ''' # INITIALIZER # - def __init__(self, relevance='certain'): + def __init__(self, relevance: str = 'certain'): self._data: list[Date] = [] - self._relevance = None # managed by property + self._relevance = '' # managed by property # not yet implemented # store an array of values marking if date data itself # is certain, approximate, or uncertain diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 009315f2c1..31db16cb3b 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -1637,7 +1637,7 @@ def parseXmlScorePart(self): # TODO: MusicXML 4.0: player tags - def getDefaultInstrument(self, mxScorePart=None): + def getDefaultInstrument(self, mxScorePart: ET.Element | None = None) -> instrument.Instrument: # noinspection PyShadowingNames r''' >>> scorePart = ('Bass' @@ -1675,6 +1675,8 @@ def getDefaultInstrument(self, mxScorePart=None): >>> mxScorePart = EL(scorePart) >>> i = PP.getDefaultInstrument(mxScorePart) + >>> i + >>> i.instrumentName 'C Trumpet' >>> i.transposition @@ -1683,6 +1685,11 @@ def getDefaultInstrument(self, mxScorePart=None): if mxScorePart is None: mxScorePart = self.mxScorePart + if mxScorePart is None: + raise MusicXMLImportException( + 'score-part must be defined before calling this.' + ) + def _adjustMidiData(mc): adjusted = int(mc) - 1 if adjusted == -1: @@ -1739,7 +1746,8 @@ def _adjustMidiData(mc): i = inst_from_name i.partId = self.partId - i.groups.append(self.partId) + if self.partId is not None: + i.groups.append(self.partId) i.partName = self.stream.partName i.partAbbreviation = self.stream.partAbbreviation # TODO: groups @@ -1803,7 +1811,7 @@ def removeEndForwardRest(self): if lmp.stream.recurse().notesAndRests.last() is endedForwardRest: lmp.stream.remove(endedForwardRest, recurse=True) - def separateOutPartStaves(self): + def separateOutPartStaves(self) -> None: ''' Take a `Part` with multiple staves and make them a set of `PartStaff` objects. From 0f466c8b00972f33ea4c8e89d7090b68035e90e3 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 6 Dec 2022 15:01:10 -1000 Subject: [PATCH 14/16] fix hidden mypy issues --- music21/base.py | 2 +- music21/common/numberTools.py | 13 +++++++++++++ music21/duration.py | 5 +++-- music21/musicxml/xmlToM21.py | 3 ++- music21/stream/core.py | 3 ++- music21/stream/makeNotation.py | 3 ++- 6 files changed, 23 insertions(+), 6 deletions(-) diff --git a/music21/base.py b/music21/base.py index 8a5bd77e00..ac7ec9ee15 100644 --- a/music21/base.py +++ b/music21/base.py @@ -3314,7 +3314,7 @@ def splitAtQuarterLength( def splitByQuarterLengths( self, - quarterLengthList: list[int | float], + quarterLengthList: list[int | float | fractions.Fraction], addTies=True, displayTiedAccidentals=False ) -> _SplitTuple: diff --git a/music21/common/numberTools.py b/music21/common/numberTools.py index e5ff33f883..09ad31052d 100644 --- a/music21/common/numberTools.py +++ b/music21/common/numberTools.py @@ -19,6 +19,7 @@ import numbers import random import typing as t +from typing import overload import unittest from music21 import defaults @@ -208,6 +209,18 @@ def _preFracLimitDenominator(n: int, d: int) -> tuple[int, int]: 0.25, 0.375, 0.5, 0.75, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0 ]) +@overload +def opFrac(num: None) -> None: + pass + +@overload +def opFrac(num: int) -> float: + pass + +@overload +def opFrac(num: float | Fraction) -> float | Fraction: + pass + # no type checking due to accessing protected attributes (for speed) @t.no_type_check def opFrac(num: OffsetQLIn | None) -> OffsetQL | None: diff --git a/music21/duration.py b/music21/duration.py index 6986458c0f..25d15e3a5a 100644 --- a/music21/duration.py +++ b/music21/duration.py @@ -263,7 +263,7 @@ def nextSmallerType(durType: str) -> str: return ordinalTypeFromNum[thisOrdinal + 1] -def quarterLengthToClosestType(qLen: OffsetQLIn): +def quarterLengthToClosestType(qLen: OffsetQLIn) -> tuple[str, bool]: ''' Returns a two-unit tuple consisting of @@ -301,6 +301,7 @@ def quarterLengthToClosestType(qLen: OffsetQLIn): music21.duration.DurationException: Cannot return types smaller than 2048th; qLen was: 0.00146484375 ''' + noteLengthType: OffsetQL if isinstance(qLen, fractions.Fraction): noteLengthType = 4 / qLen # divides right... else: @@ -784,7 +785,7 @@ def convertTypeToQuarterLength( raise DurationException( f'no such type ({dType}) available for conversion') - qtrLength = durationFromType + qtrLength: OffsetQL = durationFromType # weird medieval notational device; rarely used. if dotGroups is not None and len(dotGroups) > 1: diff --git a/music21/musicxml/xmlToM21.py b/music21/musicxml/xmlToM21.py index 31db16cb3b..fef1c7c3d2 100644 --- a/music21/musicxml/xmlToM21.py +++ b/music21/musicxml/xmlToM21.py @@ -59,6 +59,7 @@ if t.TYPE_CHECKING: from music21 import base + from music21.common.types import OffsetQL # what goes in a `.staffReference` StaffReferenceType = dict[int, list[base.Music21Object]] @@ -2403,7 +2404,7 @@ def __init__(self, self.parseIndex = 0 # what is the offset in the measure of the current note position? - self.offsetMeasureNote = 0.0 + self.offsetMeasureNote: OffsetQL = 0.0 # keep track of the last rest that was added with a forward tag. # there are many pieces that end with incomplete measures that diff --git a/music21/stream/core.py b/music21/stream/core.py index afd2b32f53..2d526f59ff 100644 --- a/music21/stream/core.py +++ b/music21/stream/core.py @@ -191,7 +191,8 @@ def coreSetElementOffset( # Note: not documenting 'highestTime' is on purpose, since can only be done for # elements already stored at end. Infinite loop. try: - offset = opFrac(offset) + # try first, for the general case of not OffsetSpecial. + offset = opFrac(offset) # type: ignore except TypeError: if offset not in OffsetSpecial: # pragma: no cover raise StreamException(f'Cannot set offset to {offset!r} for {element}') diff --git a/music21/stream/makeNotation.py b/music21/stream/makeNotation.py index 3f5a29a39b..b649f30f36 100644 --- a/music21/stream/makeNotation.py +++ b/music21/stream/makeNotation.py @@ -36,6 +36,7 @@ if t.TYPE_CHECKING: + from fractions import Fraction from music21 import stream from music21.stream.iterator import StreamIterator @@ -199,7 +200,7 @@ def makeBeams( continue # getBeams - offset = 0.0 + offset: float | Fraction = 0.0 if m.paddingLeft != 0.0: offset = opFrac(m.paddingLeft) elif m.paddingRight != 0.0: From fbb433599d03bd0dc5be20395d6b87d8800f1f70 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 6 Dec 2022 15:56:30 -1000 Subject: [PATCH 15/16] type check opFrac Maybe overload + ignore typing was causing crashes. --- music21/common/numberTools.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/music21/common/numberTools.py b/music21/common/numberTools.py index 09ad31052d..32c36ce5ca 100644 --- a/music21/common/numberTools.py +++ b/music21/common/numberTools.py @@ -222,7 +222,6 @@ def opFrac(num: float | Fraction) -> float | Fraction: pass # no type checking due to accessing protected attributes (for speed) -@t.no_type_check def opFrac(num: OffsetQLIn | None) -> OffsetQL | None: ''' opFrac -> optionally convert a number to a fraction or back. @@ -284,7 +283,7 @@ def opFrac(num: OffsetQLIn | None) -> OffsetQL | None: # (denominator & (denominator-1)) != 0 # which is a nice test, but denominator here is always a power of two... # unused_numerator, denominator = num.as_integer_ratio() # too slow - ir = num.as_integer_ratio() + ir = num.as_integer_ratio() # type: ignore if ir[1] > DENOM_LIMIT: # slightly faster[SIC!] than hard coding 65535! # _preFracLimitDenominator uses a cache return Fraction(*_preFracLimitDenominator(*ir)) # way faster! @@ -293,11 +292,14 @@ def opFrac(num: OffsetQLIn | None) -> OffsetQL | None: else: return num elif numType is int: # if vs. elif is negligible time difference. - return num + 0.0 # 8x faster than float(num) + # 8x faster than float(num) + return num + 0.0 # type: ignore elif numType is Fraction: - d = num._denominator # private access instead of property: 6x faster; may break later... + # private access instead of property: 6x faster; may break later... + d = num._denominator # type: ignore if (d & (d - 1)) == 0: # power of two... - return num._numerator / (d + 0.0) # 50% faster than float(num) + # 50% faster than float(num) + return num._numerator / (d + 0.0) # type: ignore else: return num # leave non-power of two fractions alone elif num is None: From dad8a3c2d0430331ce6234889da1a1067044136d Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 6 Dec 2022 16:11:03 -1000 Subject: [PATCH 16/16] lint --- music21/common/numberTools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music21/common/numberTools.py b/music21/common/numberTools.py index 32c36ce5ca..e9dd5b3b42 100644 --- a/music21/common/numberTools.py +++ b/music21/common/numberTools.py @@ -18,7 +18,6 @@ from math import isclose, gcd import numbers import random -import typing as t from typing import overload import unittest