From c9350c588b01ad0672e1b34144a33597f6744cf4 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 13 Jun 2024 13:27:41 -1000 Subject: [PATCH 1/8] Cleanup on TSV-DCML Converter a little cleanup of typing and fix docs --- music21/romanText/tsvConverter.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/music21/romanText/tsvConverter.py b/music21/romanText/tsvConverter.py index 20414e01d..50da831a3 100644 --- a/music21/romanText/tsvConverter.py +++ b/music21/romanText/tsvConverter.py @@ -266,7 +266,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 +521,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 +653,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 @@ -762,7 +763,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 +871,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 +913,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 +976,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 +1006,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 +1139,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') From 4df957552690542e693880b10a1c6f8eefe2bfcf Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 13 Jun 2024 13:38:13 -1000 Subject: [PATCH 2/8] lint and mypy --- .pylintrc | 2 +- music21/romanText/tsvConverter.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index c5b8cb153..e1e3a5c85 100644 --- a/.pylintrc +++ b/.pylintrc @@ -100,7 +100,7 @@ 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. # '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/romanText/tsvConverter.py b/music21/romanText/tsvConverter.py index 50da831a3..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 @@ -734,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) From 598557a602ab0c6042c0a364e64fd6df04b6754c Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 13 Jun 2024 14:02:52 -1000 Subject: [PATCH 3/8] fix newly discovered lint errors --- .pylintrc | 1 + music21/figuredBass/resolution.py | 45 +++++++++++++++++++++++-------- music21/stream/base.py | 4 +-- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/.pylintrc b/.pylintrc index e1e3a5c85..d823a13bc 100644 --- a/.pylintrc +++ b/.pylintrc @@ -101,6 +101,7 @@ disable= 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/resolution.py b/music21/figuredBass/resolution.py index 64dd8ce90..90a287f7f 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,14 @@ 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}') howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), @@ -111,7 +117,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 +177,15 @@ 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}') howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), @@ -182,12 +197,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 +256,15 @@ 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}') howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), @@ -727,14 +750,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/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): From 780dc4688971929ed7e7a281749aa1e053a91c07 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 13 Jun 2024 14:06:08 -1000 Subject: [PATCH 4/8] Update FiguredBass.Examples numbers this module is not run with every test run. Presumably some bugfix along the way slightly lowered the number of solutions when maximum separation is removed. --- music21/figuredBass/examples.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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.* From d4fb6e4cbf490b9cb6b6872ac81ca60c6d3c24e4 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 13 Jun 2024 14:11:27 -1000 Subject: [PATCH 5/8] no r --- music21/figuredBass/resolution.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/music21/figuredBass/resolution.py b/music21/figuredBass/resolution.py index 90a287f7f..a73282e95 100644 --- a/music21/figuredBass/resolution.py +++ b/music21/figuredBass/resolution.py @@ -106,7 +106,7 @@ def augmentedSixthToDominant( elif augSixthType == 2: [bass, root, unused_third, fifth, other] = augSixthChordInfo # other == seventh else: - raise ResolutionException(f'Unknown augSixthType: {augSixthType|r}') + raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), @@ -185,7 +185,7 @@ def augmentedSixthToMajorTonic( elif augSixthType == 2: [bass, root, unused_third, fifth, other] = augSixthChordInfo # other == seventh else: - raise ResolutionException(f'Unknown augSixthType: {augSixthType|r}') + raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), @@ -264,7 +264,7 @@ def augmentedSixthToMinorTonic( elif augSixthType == 2: [bass, root, unused_third, fifth, other] = augSixthChordInfo # other == seventh else: - raise ResolutionException(f'Unknown augSixthType: {augSixthType|r}') + raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), From 2b60acf036697d333cea39e20af7f4d27879509d Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 13 Jun 2024 14:49:30 -1000 Subject: [PATCH 6/8] Better typing. --- music21/figuredBass/resolution.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/music21/figuredBass/resolution.py b/music21/figuredBass/resolution.py index a73282e95..fc5cdede8 100644 --- a/music21/figuredBass/resolution.py +++ b/music21/figuredBass/resolution.py @@ -37,7 +37,7 @@ def augmentedSixthToDominant( augSixthPossib, augSixthType: int | None = None, - augSixthChordInfo: list[pitch.Pitch, None] | None = None + augSixthChordInfo: list[pitch.Pitch | None] | None = None ) -> tuple[pitch.Pitch, ...]: ''' Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3) @@ -108,6 +108,9 @@ def augmentedSixthToDominant( else: raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') + if any(x is None for x in [bass, root, fifth, other]): + raise ResolutionException(f'Chord must have bass, root, fifth, and sixth or seventh') + howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), (lambda p: p.name == fifth.name, '-m2'), @@ -187,6 +190,9 @@ def augmentedSixthToMajorTonic( else: raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') + if any(x is None for x in [bass, root, fifth, other]): + raise ResolutionException(f'Chord must have bass, root, fifth, and sixth or seventh') + howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), (lambda p: p.name == fifth.name, 'P1'), @@ -266,6 +272,9 @@ def augmentedSixthToMinorTonic( else: raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') + if any(x is None for x in [bass, root, fifth, other]): + raise ResolutionException(f'Chord must have bass, root, fifth, and sixth or seventh') + howToResolve = [(lambda p: p.name == bass.name, '-m2'), (lambda p: p.name == root.name, 'm2'), (lambda p: p.name == fifth.name, 'P1'), From c6584a9a5f8878f07abe08ac467bbab4d8732f7d Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 13 Jun 2024 14:56:41 -1000 Subject: [PATCH 7/8] lint lint lint --- music21/figuredBass/resolution.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/music21/figuredBass/resolution.py b/music21/figuredBass/resolution.py index fc5cdede8..c0e15ca80 100644 --- a/music21/figuredBass/resolution.py +++ b/music21/figuredBass/resolution.py @@ -108,8 +108,11 @@ def augmentedSixthToDominant( else: raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') - if any(x is None for x in [bass, root, fifth, other]): - raise ResolutionException(f'Chord must have bass, root, fifth, and sixth or seventh') + 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'), @@ -190,8 +193,11 @@ def augmentedSixthToMajorTonic( else: raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') - if any(x is None for x in [bass, root, fifth, other]): - raise ResolutionException(f'Chord must have bass, root, fifth, and sixth or seventh') + 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'), @@ -272,8 +278,11 @@ def augmentedSixthToMinorTonic( else: raise ResolutionException(f'Unknown augSixthType: {augSixthType!r}') - if any(x is None for x in [bass, root, fifth, other]): - raise ResolutionException(f'Chord must have bass, root, fifth, and sixth or seventh') + 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'), From 3540debdd20e4418213497dfc911b30656c0d49d Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Thu, 13 Jun 2024 15:50:38 -1000 Subject: [PATCH 8/8] last lints? --- music21/figuredBass/resolution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/music21/figuredBass/resolution.py b/music21/figuredBass/resolution.py index c0e15ca80..c16c1f70d 100644 --- a/music21/figuredBass/resolution.py +++ b/music21/figuredBass/resolution.py @@ -126,7 +126,7 @@ def augmentedSixthToDominant( def augmentedSixthToMajorTonic( augSixthPossib, augSixthType: int | None = None, - augSixthChordInfo: list[pitch.Pitch, None] | None = None + augSixthChordInfo: list[pitch.Pitch | None] | None = None ) -> tuple[pitch.Pitch, ...]: ''' Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3) @@ -212,7 +212,7 @@ def augmentedSixthToMajorTonic( def augmentedSixthToMinorTonic( augSixthPossib, augSixthType: int | None = None, - augSixthChordInfo: list[pitch.Pitch, None] | None = None + augSixthChordInfo: list[pitch.Pitch | None] | None = None ) -> tuple[pitch.Pitch, ...]: ''' Resolves French (augSixthType = 1), German (augSixthType = 2), and Swiss (augSixthType = 3)