From c4b4fc97893c26e88a245ab7743fe6741e5c6304 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Sat, 20 Jun 2026 22:37:00 -1000 Subject: [PATCH] Add type annotations to ipython21 and languageExcerpts Split out of #1937 to keep that PR reviewable. Adds parameter/return type annotations to: - ipython21/ (__init__, ipExtension, converters) - languageExcerpts/ (instrumentLookup, naturalLanguageObjects) Whole-package mypy + ruff clean; doctests pass. AI-assisted (Claude) --- music21/ipython21/__init__.py | 2 +- music21/ipython21/converters.py | 6 +++-- music21/ipython21/ipExtension.py | 16 ++++++++++--- music21/languageExcerpts/instrumentLookup.py | 6 ++--- .../naturalLanguageObjects.py | 23 +++++++++++-------- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/music21/ipython21/__init__.py b/music21/ipython21/__init__.py index f4ae079069..a34185642f 100644 --- a/music21/ipython21/__init__.py +++ b/music21/ipython21/__init__.py @@ -35,7 +35,7 @@ from music21.ipython21 import objects from music21.ipython21.ipExtension import load_ipython_extension -def loadNoMagic(): +def loadNoMagic() -> None: ''' Load the magic functions of load_ipython_extension when running Jupyter (was IPython) without needing to call a %magic function diff --git a/music21/ipython21/converters.py b/music21/ipython21/converters.py index e9dbd57a19..544a5e61fe 100644 --- a/music21/ipython21/converters.py +++ b/music21/ipython21/converters.py @@ -39,7 +39,9 @@ def showImageThroughMuseScore( *, multipageWidget: bool = False, **keywords, -): +) -> t.Any: + # Returns None in the common cases, but returns the ipywidgets interact + # callable in the multipage-widget branch, so the return type is t.Any. # noinspection PyPackageRequirements from IPython.display import Image, display, HTML # type: ignore @@ -110,7 +112,7 @@ def showImageThroughMuseScore( from ipywidgets import interact # type: ignore # pylint: disable=import-error @interact(page=(1, last_number)) - def page_display(page=1): + def page_display(page: int = 1) -> None: inner_page_fp = pages[page] if inner_page_fp.exists(): display(Image(data=inner_page_fp.read_bytes(), retina=True)) diff --git a/music21/ipython21/ipExtension.py b/music21/ipython21/ipExtension.py index b3f497c677..1adb78e6bf 100644 --- a/music21/ipython21/ipExtension.py +++ b/music21/ipython21/ipExtension.py @@ -1,12 +1,20 @@ from __future__ import annotations +import typing as t + from music21 import common +if t.TYPE_CHECKING: + # get_ipython is injected into the global namespace by IPython/Colab at + # runtime; declare it here so type checkers know the name exists. + def get_ipython() -> t.Any: + ... + _DOC_IGNORE_MODULE_OR_PACKAGE = True # See converter/subConverters/ConverterIPython for more info. -def load_ipython_extension(ip): +def load_ipython_extension(ip: t.Any) -> None: ''' Load any necessary notebook extensions. Currently just sets matplotlib to interactive mode. @@ -33,7 +41,9 @@ def load_ipython_extension(ip): pass -def inGoogleColabNotebook(): +def inGoogleColabNotebook() -> bool: + # the get_ipython() is declared for type checkers, but not pylint. + # pylint: disable=used-before-assignment if not common.runningInNotebook(): return False try: @@ -44,7 +54,7 @@ def inGoogleColabNotebook(): except NameError: return False -def notebookVersion(): +def notebookVersion() -> tuple[int, ...]: try: # noinspection PyPackageRequirements import notebook # type: ignore diff --git a/music21/languageExcerpts/instrumentLookup.py b/music21/languageExcerpts/instrumentLookup.py index 9cd3b3c97a..89a2c7b24e 100644 --- a/music21/languageExcerpts/instrumentLookup.py +++ b/music21/languageExcerpts/instrumentLookup.py @@ -1309,7 +1309,7 @@ class Test(unittest.TestCase): - def testAllToClassNamePopulated(self): + def testAllToClassNamePopulated(self) -> None: ''' Test that the allToClassName dict includes all the keys from the constituent dicts. @@ -1327,7 +1327,7 @@ def testAllToClassNamePopulated(self): for key in eachDict: self.assertIn(key, allToClassName) - def testAllToClassNameExamples(self): + def testAllToClassNameExamples(self) -> None: ''' Test an example from each constituent dict that makes up allToClassName. ''' @@ -1342,7 +1342,7 @@ def testAllToClassNameExamples(self): ]: self.assertEqual(allToClassName[testString], langDict[testString]) - def testAllClassNames(self): + def testAllClassNames(self) -> None: ''' Test that all class names are real. ''' diff --git a/music21/languageExcerpts/naturalLanguageObjects.py b/music21/languageExcerpts/naturalLanguageObjects.py index a9a64f0685..40c8caff8b 100644 --- a/music21/languageExcerpts/naturalLanguageObjects.py +++ b/music21/languageExcerpts/naturalLanguageObjects.py @@ -12,16 +12,21 @@ ''' from __future__ import annotations +import typing as t import unittest from music21 import pitch +if t.TYPE_CHECKING: + from music21 import chord + from music21 import note + SUPPORTED_LANGUAGES = ['de', 'fr', 'it', 'es'] SUPPORTED_ACCIDENTALS = ['----', '---', '--', '-', '', '#', '##', '###', '####'] SUPPORTED_MICROTONES = ['A', 'B', 'C', 'D', 'E', 'F', 'G'] -def generateLanguageDictionary(languageString): +def generateLanguageDictionary(languageString: str) -> dict[str, str]: # Helper method for toPitch @@ -31,8 +36,8 @@ def generateLanguageDictionary(languageString): if languageString not in SUPPORTED_LANGUAGES: return {} - dictionary = {} - pitchStrings = [] + dictionary: dict[str, str] = {} + pitchStrings: list[str] = [] for microtone in SUPPORTED_MICROTONES: for accidental in SUPPORTED_ACCIDENTALS: @@ -58,7 +63,7 @@ def generateLanguageDictionary(languageString): return dictionary -def toPitch(pitchString, languageString): +def toPitch(pitchString: str, languageString: str) -> pitch.Pitch: ''' Converts a string to a :class:`music21.pitch.Pitch` object given a language. @@ -84,7 +89,7 @@ def toPitch(pitchString, languageString): return pitch.Pitch(langDict[pitchString]) -def toNote(pitchString, languageString): +def toNote(pitchString: str, languageString: str) -> note.Note: ''' Converts a string to a :class:`music21.note.Note` object given a language @@ -109,7 +114,7 @@ def toNote(pitchString, languageString): return note.Note(toPitch(pitchString, languageString)) -def toChord(pitchArray, languageString): +def toChord(pitchArray: list[str], languageString: str) -> chord.Chord: ''' Converts a list of strings to a :class:`music21.chord.Chord` object given a language @@ -130,7 +135,7 @@ def toChord(pitchArray, languageString): class Test(unittest.TestCase): - def testConvertPitches(self): + def testConvertPitches(self) -> None: # testing defaults in case of invalid language and invalid input self.assertEqual('', repr(toPitch('hello', ''))) self.assertEqual('', repr(toPitch('', 'hello'))) @@ -171,7 +176,7 @@ def testConvertPitches(self): repr(toPitch('la quadruple dièse', 'fr'))) self.assertEqual('', repr(toPitch('si triple bémol', 'fr'))) - def testConvertNotes(self): + def testConvertNotes(self) -> None: # testing defaults in case of invalid language and invalid input self.assertEqual('', repr(toNote('hello', ''))) self.assertEqual('', repr(toNote('', 'hello'))) @@ -209,7 +214,7 @@ def testConvertNotes(self): repr(toNote('la quadruple dièse', 'fr'))) self.assertEqual('', repr(toNote('si triple bémol', 'fr'))) - def testConvertChords(self): + def testConvertChords(self) -> None: # testing defaults in case of invalid language and no input self.assertEqual((), toChord([], '').pitches) self.assertEqual((), toChord([], 'hello').pitches)