diff --git a/.pylintrc b/.pylintrc index c5b8cb153..d823a13bc 100644 --- a/.pylintrc +++ b/.pylintrc @@ -100,7 +100,8 @@ disable= consider-using-f-string, # future? unnecessary-lambda-assignment, # opinionated consider-using-generator, # generators are less performant for small container sizes, like most of ours - + not-an-iterable, # false positives on RecursiveIterator in recent pylint versions. + unpacking-non-sequence, # also getting false positives. # 'protected-access', # this is an important one, but for now we do a lot of # # x = copy.deepcopy(self); x._volume = ... which is not a problem... diff --git a/music21/figuredBass/examples.py b/music21/figuredBass/examples.py index d2d1abc2e..a6846546d 100644 --- a/music21/figuredBass/examples.py +++ b/music21/figuredBass/examples.py @@ -67,7 +67,7 @@ def exampleA(): >>> fbRealization2 = fbLine.realize(fbRules) >>> fbRealization2.keyboardStyleOutput = False >>> fbRealization2.getNumSolutions() - 3713168 + 3564440 >>> #_DOCS_SHOW fbRealization2.generateRandomRealization().show() .. image:: images/figuredBass/fbExamples_sol2A.* @@ -111,7 +111,6 @@ def exampleD(): figured bass, and fbLine is realized again. Voice overlap can be seen in the fourth measure. - >>> fbRules.forbidVoiceOverlap = False >>> fbRealization2 = fbLine.realize(fbRules) >>> fbRealization2.getNumSolutions() @@ -124,12 +123,11 @@ def exampleD(): Now, the restriction on voice overlap is reset, but the restriction on the upper parts being within a perfect octave of each other is removed. fbLine is realized again. - >>> fbRules.forbidVoiceOverlap = True >>> fbRules.upperPartsMaxSemitoneSeparation = None >>> fbRealization3 = fbLine.realize(fbRules) >>> fbRealization3.getNumSolutions() - 29629539 + 27445876 >>> fbRealization3.keyboardStyleOutput = False >>> #_DOCS_SHOW fbRealization3.generateRandomRealization().show() @@ -177,7 +175,7 @@ def exampleB(): >>> fbRules.forbidIncompletePossibilities = False >>> fbRealization2 = fbLine.realize(fbRules) >>> fbRealization2.getNumSolutions() - 188974 + 159373 >>> #_DOCS_SHOW fbRealization2.generateRandomRealization().show() .. image:: images/figuredBass/fbExamples_sol2B.* diff --git a/music21/figuredBass/resolution.py b/music21/figuredBass/resolution.py index 64dd8ce90..c16c1f70d 100644 --- a/music21/figuredBass/resolution.py +++ b/music21/figuredBass/resolution.py @@ -8,10 +8,8 @@ # License: BSD, see license.txt # ------------------------------------------------------------------------------ ''' -.. note:: The terminology, V43, viio, iv, etc. are explained - more fully in *The Music Theory Handbook* - by Marjorie Merryman. - +.. note:: The terminology, V43, viio, iv, etc. are explained elsewhere, + such as *The Music Theory Handbook* by Marjorie Merryman. This module contains methods which can properly resolve `dominant seventh `_, @@ -26,6 +24,7 @@ ''' from __future__ import annotations +import typing as t import unittest from music21 import exceptions21 @@ -35,12 +34,15 @@ from music21 import stream -def augmentedSixthToDominant(augSixthPossib, augSixthType=None, augSixthChordInfo=None): +def augmentedSixthToDominant( + augSixthPossib, + augSixthType: int | None = None, + augSixthChordInfo: list[pitch.Pitch | None] | None = None +) -> tuple[pitch.Pitch, ...]: ''' Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3) augmented sixth chords to the root position dominant triad. - Proper Italian augmented sixth resolutions not supported within this method. >>> from music21.figuredBass import resolution @@ -97,10 +99,20 @@ def augmentedSixthToDominant(augSixthPossib, augSixthType=None, augSixthChordInf elif augSixthChord.isSwissAugmentedSixth(): augSixthType = 3 + if t.TYPE_CHECKING: + assert augSixthChordInfo is not None if augSixthType in (1, 3): [bass, other, root, unused_third, fifth] = augSixthChordInfo # other == sixth elif augSixthType == 2: [bass, root, unused_third, fifth, other] = augSixthChordInfo # other == seventh + else: + raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') + + if t.TYPE_CHECKING: + assert isinstance(bass, pitch.Pitch) + assert isinstance(root, pitch.Pitch) + assert isinstance(fifth, pitch.Pitch) + assert isinstance(other, pitch.Pitch) howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), @@ -111,7 +123,11 @@ def augmentedSixthToDominant(augSixthPossib, augSixthType=None, augSixthChordInf return _resolvePitches(augSixthPossib, howToResolve) -def augmentedSixthToMajorTonic(augSixthPossib, augSixthType=None, augSixthChordInfo=None): +def augmentedSixthToMajorTonic( + augSixthPossib, + augSixthType: int | None = None, + augSixthChordInfo: list[pitch.Pitch | None] | None = None +) -> tuple[pitch.Pitch, ...]: ''' Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3) augmented sixth chords to the major tonic 6,4. @@ -167,10 +183,21 @@ def augmentedSixthToMajorTonic(augSixthPossib, augSixthType=None, augSixthChordI elif augSixthChord.isSwissAugmentedSixth(): augSixthType = 3 + if t.TYPE_CHECKING: + assert augSixthChordInfo is not None + if augSixthType in (1, 3): [bass, other, root, unused_third, fifth] = augSixthChordInfo # other == sixth elif augSixthType == 2: [bass, root, unused_third, fifth, other] = augSixthChordInfo # other == seventh + else: + raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') + + if t.TYPE_CHECKING: + assert isinstance(bass, pitch.Pitch) + assert isinstance(root, pitch.Pitch) + assert isinstance(fifth, pitch.Pitch) + assert isinstance(other, pitch.Pitch) howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), @@ -182,12 +209,15 @@ def augmentedSixthToMajorTonic(augSixthPossib, augSixthType=None, augSixthChordI return _resolvePitches(augSixthPossib, howToResolve) -def augmentedSixthToMinorTonic(augSixthPossib, augSixthType=None, augSixthChordInfo=None): +def augmentedSixthToMinorTonic( + augSixthPossib, + augSixthType: int | None = None, + augSixthChordInfo: list[pitch.Pitch | None] | None = None +) -> tuple[pitch.Pitch, ...]: ''' Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3) augmented sixth chords to the minor tonic 6,4. - Proper Italian augmented sixth resolutions not supported within this method. >>> from music21.figuredBass import resolution @@ -238,10 +268,21 @@ def augmentedSixthToMinorTonic(augSixthPossib, augSixthType=None, augSixthChordI elif augSixthChord.isSwissAugmentedSixth(): augSixthType = 3 + if t.TYPE_CHECKING: + assert augSixthChordInfo is not None + if augSixthType in (1, 3): [bass, other, root, unused_third, fifth] = augSixthChordInfo # other == sixth elif augSixthType == 2: [bass, root, unused_third, fifth, other] = augSixthChordInfo # other == seventh + else: + raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') + + if t.TYPE_CHECKING: + assert isinstance(bass, pitch.Pitch) + assert isinstance(root, pitch.Pitch) + assert isinstance(fifth, pitch.Pitch) + assert isinstance(other, pitch.Pitch) howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), @@ -727,14 +768,14 @@ def _transpose(samplePitch, intervalString): return samplePitch.transpose(intervalString) -def _resolvePitches(possibToResolve, howToResolve): +def _resolvePitches(possibToResolve, howToResolve) -> tuple[pitch.Pitch, ...]: ''' Takes in a possibility to resolve and a list of (lambda function, intervalString) pairs and transposes each pitch by the intervalString corresponding to the lambda function that returns True when applied to the pitch. ''' howToResolve.append((lambda p: True, 'P1')) - resPitches = [] + resPitches: list[pitch.Pitch] = [] for samplePitch in possibToResolve: for (expression, intervalString) in howToResolve: if expression(samplePitch): diff --git a/music21/romanText/tsvConverter.py b/music21/romanText/tsvConverter.py index 20414e01d..b98844377 100644 --- a/music21/romanText/tsvConverter.py +++ b/music21/romanText/tsvConverter.py @@ -17,6 +17,7 @@ import abc import csv import fractions +import pathlib import re import string import types @@ -266,7 +267,7 @@ def _changeRepresentation(self) -> None: self.chord = re.sub( r''' (\d+) # match one or more digits - (?![\]\d]) # without a digit or a ']' to the right + (?![]\d]) # without a digit or a ']' to the right ''', r'd\1', self.chord, @@ -521,7 +522,7 @@ class TsvHandler: 'I' ''' - def __init__(self, tsvFile: str, dcml_version: int = 1): + def __init__(self, tsvFile: str|pathlib.Path, dcml_version: int = 1): if dcml_version == 1: self.heading_names = HEADERS[1] self._tab_chord_cls: type[TabChordBase] = TabChord @@ -653,6 +654,7 @@ def prepStream(self) -> stream.Score: ''' s = stream.Score() p = stream.Part() + m: stream.Measure|None = None if self.dcml_version == 1: # This sort of metadata seems to have been removed altogether from the # v2 files @@ -733,7 +735,7 @@ def prepStream(self) -> stream.Score: currentMeasureLength = newTS.barDuration.quarterLength previousMeasure = entry.measure - if repeatBracket is not None: + if repeatBracket is not None and m is not None: # m should always be not None... repeatBracket.addSpannedElements(m) s.append(p) @@ -762,7 +764,6 @@ class M21toTSV: >>> tsvData[1][DCML_V2_HEADERS.index('chord')] 'I' ''' - def __init__(self, m21Stream: stream.Score, dcml_version: int = 2): self.version = dcml_version self.m21Stream = m21Stream @@ -871,6 +872,8 @@ def _m21ToTsv_v2(self) -> list[list[str]]: thisEntry.numeral = '@none' thisEntry.chord = '@none' else: + if t.TYPE_CHECKING: + assert isinstance(thisRN, roman.RomanNumeral) local_key = localKeyAsRn(thisRN.key, global_key_obj) relativeroot = None if thisRN.secondaryRomanNumeral: @@ -911,7 +914,7 @@ def _m21ToTsv_v2(self) -> list[list[str]]: tsvData.append(thisInfo) return tsvData - def write(self, filePathAndName: str): + def write(self, filePathAndName: str|pathlib.Path): ''' Writes a list of lists (e.g. from m21ToTsv()) to a tsv file. ''' @@ -974,16 +977,19 @@ def handleAddedTones(dcmlChord: str) -> str: 'Viio7[no3][no5][addb4]/V' When in root position, 7 does not replace 8: + >>> romanText.tsvConverter.handleAddedTones('vi(#74)') 'vi[no3][add#7][add4]' When not in root position, 7 does replace 8: + >>> romanText.tsvConverter.handleAddedTones('ii6(11#7b6)') 'ii6[no8][no5][add11][add#7][addb6]' '0' can be used to indicate root-replacement by 7 in a root-position chord. We need to change '0' to '7' because music21 changes the 0 to 'o' (i.e., a diminished chord). + >>> romanText.tsvConverter.handleAddedTones('i(#0)') 'i[no1][add#7]' ''' @@ -1001,8 +1007,8 @@ def handleAddedTones(dcmlChord: str) -> str: return 'Cad64' + secondary added_tone_tuples: list[tuple[str, str, str, str]] = re.findall( r''' - (\+|-)? # indicates whether to add or remove chord factor - (\^|v)? # indicates whether tone replaces chord factor above/below + ([+\-])? # indicates whether to add or remove chord factor + ([\^v])? # indicates whether tone replaces chord factor above/below (\#+|b+)? # alteration (1\d|\d) # figures 0-19, in practice 0-14 ''', @@ -1134,7 +1140,6 @@ def getLocalKey(local_key: str, global_key: str, convertDCMLToM21: bool = False) >>> romanText.tsvConverter.getLocalKey('vii', 'a', convertDCMLToM21=True) 'g' - ''' if convertDCMLToM21: local_key = characterSwaps(local_key, minor=isMinor(global_key[0]), direction='DCML-m21') diff --git a/music21/stream/base.py b/music21/stream/base.py index e407600f6..0d0db5fc9 100644 --- a/music21/stream/base.py +++ b/music21/stream/base.py @@ -4137,7 +4137,7 @@ def getElementAtOrBefore( # TODO: allow sortTuple as a parameter (in all getElement...) candidates = [] - offset = opFrac(offset) + offset: OffsetQL = opFrac(offset) nearestTrailSpan = offset # start with max time sIterator = self.iter() @@ -4147,7 +4147,7 @@ def getElementAtOrBefore( # need both _elements and _endElements for e in sIterator: - span = opFrac(offset - self.elementOffset(e)) + span: OffsetQL = opFrac(offset - self.elementOffset(e)) # environLocal.printDebug(['e span check', span, 'offset', offset, # 'e.offset', e.offset, 'self.elementOffset(e)', self.elementOffset(e), 'e', e]) if span < 0 or (span == 0 and _beforeNotAt):