From 504191a988d6e7aff28091e6bd79f26d8734c99d Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 16 Jun 2026 17:08:32 -1000 Subject: [PATCH 1/3] midi: add full type annotations to the implementation modules Annotate every parameter and return type across the MIDI implementation (base, percussion, realtime.StreamPlayer, translate). All four pass mypy under --disallow-untyped-defs --disallow-incomplete-defs. Notable supporting changes (all behavior-preserving, verified by the existing doctests and tests.py): - base.MidiFile.open() now accepts str|pathlib.Path (it is already called with Path objects). - The three meta-event readers narrow with isinstance(eventList, MidiEvent) instead of common.isListLike(), giving mypy proper narrowing. - assignPacketsToChannels() uses track=None for the conductor pitch-bend event (the inline comment already stated this intent; the track is overwritten by MidiTrack.updateEvents() before serialization) and skips tracks with no assigned channel. - midiAsciiStringToBinaryString() guards against tracksEventsList=None. - percussion attribute accesses that live on Instrument subclasses are narrowed with TYPE_CHECKING asserts. Test code (tests.py and the disabled x_test* scaffolds in realtime) is left untyped, by request. AI-assisted (Claude) --- music21/midi/base.py | 21 +++--- music21/midi/percussion.py | 12 +++- music21/midi/realtime.py | 38 ++++++----- music21/midi/translate.py | 135 ++++++++++++++++++++++--------------- 4 files changed, 124 insertions(+), 82 deletions(-) diff --git a/music21/midi/base.py b/music21/midi/base.py index 4198decf2b..c364448f3d 100644 --- a/music21/midi/base.py +++ b/music21/midi/base.py @@ -44,6 +44,9 @@ from typing import overload import typing as t +if t.TYPE_CHECKING: + import pathlib + from music21.common.enums import ContainsEnum from music21 import defaults from music21 import environment @@ -491,7 +494,7 @@ def __init__(self, track: MidiTrack|None = None, type: MidiEventTypes = MetaEvents.UNKNOWN, time: int = 0, - channel: int = 1): + channel: int = 1) -> None: self.track: MidiTrack|None = track # a MidiTrack object self.type: MidiEventTypes = type self.time: int = time @@ -585,7 +588,7 @@ def pitch(self) -> int|None: return None @pitch.setter - def pitch(self, value: int|None): + def pitch(self, value: int|None) -> None: self.parameter1 = value @property @@ -595,7 +598,7 @@ def velocity(self) -> int|None: return self.parameter2 @velocity.setter - def velocity(self, value: int|None): + def velocity(self, value: int|None) -> None: self.parameter2 = value # store generic data in parameter 1 @@ -614,7 +617,7 @@ def data(self) -> int|bytes|None: return self.parameter1 @data.setter - def data(self, value: int|str|bytes|bool|None): + def data(self, value: int|str|bytes|bool|None) -> None: if value is not None and not isinstance(value, bytes): if isinstance(value, str): value = value.encode('utf-8') @@ -635,7 +638,7 @@ def isChannelEvent(self) -> bool: ''' return self.type in ChannelVoiceMessages or self.type in ChannelModeMessages - def setPitchBend(self, cents: int|float, bendRange=2) -> None: + def setPitchBend(self, cents: int|float, bendRange: int = 2) -> None: ''' Treat this event as a pitch bend value, and set the .parameter1 and .parameter2 fields appropriately given a specified bend value in cents. @@ -1244,7 +1247,7 @@ def __init__( track: MidiTrack|None = None, time: int = 0, channel: int = 1, - ): + ) -> None: super().__init__(track, time=time, channel=channel) self.type = 'DeltaTime' @@ -1388,7 +1391,7 @@ class MidiTrack(prebase.ProtoM21Object): ''' headerId = b'MTrk' - def __init__(self, index: int = 0): + def __init__(self, index: int = 0) -> None: self.index = index self.events: list[MidiEvent] = [] # or DeltaTime subclass self.data = b'' @@ -1550,7 +1553,7 @@ def getBytes(self) -> bytes: bytes_out = b''.join(midiBytes) return self.headerId + putNumber(len(bytes_out), 4) + bytes_out - def _reprInternal(self): + def _reprInternal(self) -> str: r = f'{self.index} -- {len(self.events)} events' return r @@ -1713,7 +1716,7 @@ def __init__(self) -> None: self.ticksPerQuarterNote: int = defaults.ticksPerQuarter self.ticksPerSecond: int|None = None - def open(self, filename, attrib='rb') -> None: + def open(self, filename: str|pathlib.Path, attrib: str = 'rb') -> None: ''' Open a MIDI file path for reading or writing. diff --git a/music21/midi/percussion.py b/music21/midi/percussion.py index 6e70f85e4b..6e580ed559 100644 --- a/music21/midi/percussion.py +++ b/music21/midi/percussion.py @@ -10,6 +10,7 @@ # ------------------------------------------------------------------------------ from __future__ import annotations +import typing as t import unittest from music21 import pitch @@ -94,7 +95,7 @@ class PercussionMapper: # formerly at: # https://www.midi.org/specifications/item/gm-level-1-sound-set - def midiPitchToInstrument(self, midiPitch): + def midiPitchToInstrument(self, midiPitch: int|pitch.Pitch) -> instrument.Instrument: ''' Takes a pitch.Pitch object or int ranging from 0-127 and returns the corresponding instrument in the GM Percussion Map. @@ -157,13 +158,15 @@ def midiPitchToInstrument(self, midiPitch): midiInstrumentObject = midiInstrument() if (midiInstrumentObject.inGMPercMap is True and hasattr(midiInstrumentObject, '_percMapPitchToModifier')): + if t.TYPE_CHECKING: + assert isinstance(midiInstrumentObject, instrument.UnpitchedPercussion) if midiNumber in midiInstrumentObject._percMapPitchToModifier: modifier = midiInstrumentObject._percMapPitchToModifier[midiNumber] midiInstrumentObject.modifier = modifier return midiInstrumentObject - def midiInstrumentToPitch(self, midiInstrument): + def midiInstrumentToPitch(self, midiInstrument: instrument.Instrument) -> pitch.Pitch: ''' Takes an instrument.Instrument object and returns a pitch object with the corresponding 1-indexed MIDI note, according to the GM Percussion Map. @@ -191,6 +194,9 @@ def midiInstrumentToPitch(self, midiInstrument): ''' if not hasattr(midiInstrument, 'inGMPercMap') or midiInstrument.inGMPercMap is False: raise MIDIPercussionException(f'{midiInstrument!r} is not in the GM Percussion Map!') + if t.TYPE_CHECKING: + assert isinstance(midiInstrument, instrument.Percussion) + assert midiInstrument.percMapPitch is not None midiPitch = midiInstrument.percMapPitch pitchObject = pitch.Pitch() pitchObject.midi = midiPitch @@ -201,7 +207,7 @@ def midiInstrumentToPitch(self, midiInstrument): class Test(unittest.TestCase): - def testCopyAndDeepcopy(self): + def testCopyAndDeepcopy(self) -> None: from music21.test.commonTest import testCopyAll testCopyAll(self, globals()) diff --git a/music21/midi/realtime.py b/music21/midi/realtime.py index 206ec2e819..d84247bf68 100644 --- a/music21/midi/realtime.py +++ b/music21/midi/realtime.py @@ -21,8 +21,10 @@ ''' from __future__ import annotations +from collections.abc import Callable from importlib.util import find_spec from io import BytesIO +import typing as t import unittest from music21 import defaults @@ -80,7 +82,7 @@ def __init__( mixerBitSize: int = -16, mixerChannels: int = 2, mixerBuffer: int = 1024, - ): + ) -> None: try: # noinspection PyPackageRequirements import pygame # type: ignore @@ -93,14 +95,14 @@ def __init__( self.streamIn = streamIn def play(self, - busyFunction=None, - busyArgs=None, - endFunction=None, - endArgs=None, - busyWaitMilliseconds=50, + busyFunction: Callable[[t.Any], t.Any]|None = None, + busyArgs: t.Any = None, + endFunction: Callable[[t.Any], t.Any]|None = None, + endArgs: t.Any = None, + busyWaitMilliseconds: int = 50, *, - playForMilliseconds=float('inf'), - blocked=True): + playForMilliseconds: float = float('inf'), + blocked: bool = True) -> None: ''' busyFunction is a function that is called with busyArgs when the music is busy every busyWaitMilliseconds. @@ -123,15 +125,21 @@ def play(self, playForMilliseconds=playForMilliseconds, blocked=blocked) - def getStringOrBytesIOFile(self): + def getStringOrBytesIOFile(self) -> BytesIO: streamMidiFile = midiTranslate.streamToMidiFile(self.streamIn) streamMidiWritten = streamMidiFile.writestr() return BytesIO(streamMidiWritten) - def playStringIOFile(self, stringIOFile, busyFunction=None, busyArgs=None, - endFunction=None, endArgs=None, busyWaitMilliseconds=50, + def playStringIOFile(self, + stringIOFile: BytesIO, + busyFunction: Callable[[t.Any], t.Any]|None = None, + busyArgs: t.Any = None, + endFunction: Callable[[t.Any], t.Any]|None = None, + endArgs: t.Any = None, + busyWaitMilliseconds: int = 50, *, - playForMilliseconds=float('inf'), blocked=True): + playForMilliseconds: float = float('inf'), + blocked: bool = True) -> None: ''' busyFunction is a function that is called with busyArgs when the music is busy every busyWaitMilliseconds. @@ -167,7 +175,7 @@ def playStringIOFile(self, stringIOFile, busyFunction=None, busyArgs=None, if endFunction is not None: endFunction(endArgs) - def stop(self): + def stop(self) -> None: self.pygame.mixer.music.stop() @@ -183,7 +191,7 @@ class TestExternal(unittest.TestCase): # pragma: no cover pygame_installed = False @unittest.skipUnless(pygame_installed, 'pygame is not installed') - def testBachDetune(self): + def testBachDetune(self) -> None: from music21 import corpus import random b = corpus.parse('bwv66.6') @@ -232,7 +240,7 @@ class Mock: sp = StreamPlayer(b) sp.play(busyFunction=busyCounter, busyArgs=[timeCounter], busyWaitMilliseconds=500) - def x_testPlayOneMeasureAtATime(self): + def x_testPlayOneMeasureAtATime(self) -> None: from music21 import corpus defaults.ticksAtStart = 0 b = corpus.parse('bwv66.6') diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 83fdb99e6a..687a35c5ed 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -48,6 +48,7 @@ if t.TYPE_CHECKING: + import pathlib from music21 import base from music21.common.types import OffsetQLIn @@ -300,8 +301,8 @@ def getEndEvents( def music21ObjectToMidiFile( music21Object: base.Music21Object, *, - addStartDelay=False, - addEndDelay=True, + addStartDelay: bool = False, + addEndDelay: bool = True, encoding: str = 'utf-8', ) -> MidiFile: ''' @@ -450,8 +451,11 @@ def midiEventsToNote( if isinstance(nr, note.Note): nr.pitch.midi = midiOnEvent.pitch elif isinstance(nr, note.Unpitched): + onPitch = midiOnEvent.pitch + if t.TYPE_CHECKING: + assert onPitch is not None try: - i = PERCUSSION_MAPPER.midiPitchToInstrument(midiOnEvent.pitch) + i = PERCUSSION_MAPPER.midiPitchToInstrument(onPitch) except MIDIPercussionException: i = instrument.UnpitchedPercussion() nr.storedInstrument = i @@ -845,7 +849,7 @@ def instrumentToMidiEvents( includeDeltaTime: bool = True, midiTrack: MidiTrack|None = None, channel: int = 1, -): +) -> list[MidiEvent|DeltaTime]: ''' Converts a :class:`~music21.instrument.Instrument` object to a list of MidiEvents @@ -975,7 +979,9 @@ def midiEventToInstrument( return i -def midiEventsToTimeSignature(eventList): +def midiEventsToTimeSignature( + eventList: MidiEvent|Sequence[MidiEvent] +) -> meter.TimeSignature: # noinspection PyShadowingNames ''' Convert a single MIDI event into a music21 TimeSignature object. @@ -1016,12 +1022,14 @@ def midiEventsToTimeSignature(eventList): # Time Signature Event should appear in the first track chunk (or all track # chunks in a Type 2 file) before any non-zero delta time events. If one # is not specified 4/4, 24, 8 should be assumed. - if not common.isListLike(eventList): + if isinstance(eventList, MidiEvent): event = eventList else: # get the second event; first is delta time event = eventList[1] # time signature is 4 byte encoding + if t.TYPE_CHECKING: + assert isinstance(event.data, bytes) post = list(event.data) n = post[0] @@ -1030,7 +1038,10 @@ def midiEventsToTimeSignature(eventList): return ts -def timeSignatureToMidiEvents(ts, includeDeltaTime=True) -> list[DeltaTime|MidiEvent]: +def timeSignatureToMidiEvents( + ts: meter.TimeSignature, + includeDeltaTime: bool = True +) -> list[DeltaTime|MidiEvent]: # noinspection PyShadowingNames ''' Translate a :class:`~music21.meter.TimeSignature` to a pair of events: a DeltaTime and @@ -1089,7 +1100,7 @@ def timeSignatureToMidiEvents(ts, includeDeltaTime=True) -> list[DeltaTime|MidiE return eventList -def midiEventsToKey(eventList) -> key.Key: +def midiEventsToKey(eventList: MidiEvent|Sequence[MidiEvent]) -> key.Key: # noinspection PyShadowingNames r''' Convert a single MIDI event into a :class:`~music21.key.KeySignature` object. @@ -1124,10 +1135,12 @@ def midiEventsToKey(eventList) -> key.Key: # the key specifies the number of sharps and a negative value specifies # the number of flats. A value of 0 for the scale specifies a major key # and a value of 1 specifies a minor key. - if not common.isListLike(eventList): + if isinstance(eventList, MidiEvent): event = eventList else: # get the second event; first is delta time event = eventList[1] + if t.TYPE_CHECKING: + assert isinstance(event.data, bytes) post = list(event.data) # first value is number of sharp, or neg for number of flat @@ -1150,7 +1163,7 @@ def midiEventsToKey(eventList) -> key.Key: def keySignatureToMidiEvents( ks: key.KeySignature, - includeDeltaTime=True + includeDeltaTime: bool = True ) -> list[DeltaTime|MidiEvent]: # noinspection PyShadowingNames r''' @@ -1211,17 +1224,19 @@ def keySignatureToMidiEvents( return eventList -def midiEventsToTempo(eventList): +def midiEventsToTempo(eventList: MidiEvent|Sequence[MidiEvent]) -> tempo.MetronomeMark: ''' Convert a single MIDI event into a music21 Tempo object. TODO: Need Tests ''' - if not common.isListLike(eventList): + if isinstance(eventList, MidiEvent): event = eventList else: # get the second event; first is delta time event = eventList[1] # get microseconds per quarter + if t.TYPE_CHECKING: + assert isinstance(event.data, bytes) mspq = getNumber(event.data, 3)[0] # first data is number bpm = round(60_000_000 / mspq, 2) # post = list(event.data) @@ -1232,7 +1247,7 @@ def midiEventsToTempo(eventList): def tempoToMidiEvents( tempoIndication: tempo.MetronomeMark, - includeDeltaTime=True, + includeDeltaTime: bool = True, ) -> list[DeltaTime|MidiEvent]|None: # noinspection PyShadowingNames r''' @@ -1503,11 +1518,11 @@ def streamToPackets( def assignPacketsToChannels( - packets, - channelByInstrument=None, - channelsDynamic=None, - initTrackIdToChannelMap=None, -): + packets: list[dict[str, t.Any]], + channelByInstrument: dict[int|None, int]|None = None, + channelsDynamic: list[int]|None = None, + initTrackIdToChannelMap: dict[int, int|None]|None = None, +) -> list[dict[str, t.Any]]: ''' Given a list of packets, assign each to a channel. @@ -1534,9 +1549,10 @@ def assignPacketsToChannels( if initTrackIdToChannelMap is None: initTrackIdToChannelMap = {} - uniqueChannelEvents = {} # dict of (start, stop, usedChannel) : channel - post = [] - usedTracks = [] + # dict of (start, stop, usedChannel) : list of cent shifts + uniqueChannelEvents: dict[tuple[int, int, int], list[int]] = {} + post: list[dict[str, t.Any]] = [] + usedTracks: list[int] = [] for p in packets: # environLocal.printDebug(['assignPacketsToChannels', p['midiEvent'].track, p['trackId']]) @@ -1703,8 +1719,10 @@ def assignPacketsToChannels( if trackId == 0: continue # Conductor track: do not add pitch bend ch = initTrackIdToChannelMap[trackId] - # use None for track; will get updated later - me = MidiEvent(track=trackId, + if ch is None: + continue # no channel assigned: do not add pitch bend + # use None for track; will get updated later by MidiTrack.updateEvents() + me = MidiEvent(track=None, type=ChannelVoiceMessages.PITCH_BEND, channel=ch) me.setPitchBend(0) @@ -1796,7 +1814,14 @@ def packetsToDeltaSeparatedEvents( return events -def packetsToMidiTrack(packets, trackId=1, channel=1, instrumentObj=None, *, addEndDelay=True): +def packetsToMidiTrack( + packets: list[dict[str, t.Any]], + trackId: int = 1, + channel: int = 1, + instrumentObj: instrument.Instrument|None = None, + *, + addEndDelay: bool = True, +) -> MidiTrack: ''' Given packets already allocated with channel and/or instrument assignments, place these in a MidiTrack. @@ -2029,7 +2054,7 @@ def insertConductorEvents( target: stream.Part, *, isFirst: bool = False, -): +) -> None: ''' Insert a deepcopy of any TimeSignature, KeySignature, or MetronomeMark found in the `conductorPart` into the `target` Part at the same offset. @@ -2047,16 +2072,16 @@ def insertConductorEvents( target.insert(conductorPart.elementOffset(e), eventCopy) def midiTrackToStream( - mt, + mt: MidiTrack, *, ticksPerQuarter: int = defaults.ticksPerQuarter, - quantizePost=True, - inputM21=None, + quantizePost: bool = True, + inputM21: stream.Part|None = None, conductorPart: stream.Part|None = None, isFirst: bool = False, quarterLengthDivisors: Sequence[int] = (), encoding: str = 'utf-8', - **keywords + **keywords: t.Any ) -> stream.Part: # noinspection PyShadowingNames ''' @@ -2258,7 +2283,7 @@ def midiTrackToStream( return s -def prepareStreamForMidi(s) -> stream.Stream: +def prepareStreamForMidi(s: stream.Stream) -> stream.Stream: # noinspection PyShadowingNames ''' Given a score, prepare it for MIDI processing, and return a new Stream: @@ -2510,9 +2535,9 @@ def channelInstrumentData( def packetStorageFromSubstreamList( - substreamList: list[stream.Part], + substreamList: Sequence[stream.Stream], *, - addStartDelay=False, + addStartDelay: bool = False, encoding: str = 'utf-8', ) -> dict[int, dict[str, t.Any]]: # noinspection PyShadowingNames @@ -2613,7 +2638,7 @@ def packetStorageFromSubstreamList( def updatePacketStorageWithChannelInfo( packetStorage: dict[int, dict[str, t.Any]], - channelByInstrument: dict[int|None, int|None], + channelByInstrument: dict[int|None, int], ) -> None: ''' Take the packetStorage dictionary and using information @@ -2644,13 +2669,13 @@ def updatePacketStorageWithChannelInfo( def streamHierarchyToMidiTracks( - inputM21, + inputM21: stream.Stream, *, - acceptableChannelList=None, - addStartDelay=False, - addEndDelay=True, - encoding='utf-8', -): + acceptableChannelList: list[int]|None = None, + addStartDelay: bool = False, + addEndDelay: bool = True, + encoding: str = 'utf-8', +) -> list[MidiTrack]: ''' Given a Stream, Score, Part, etc., that may have substreams (i.e., a hierarchy), return a list of :class:`~music21.midi.MidiTrack` objects. @@ -2688,7 +2713,7 @@ def streamHierarchyToMidiTracks( # Streams that do not start at same time # store streams in uniform list: prepareStreamForMidi() ensures there are substreams - substreamList = [] + substreamList: list[stream.Stream] = [] for obj in s.getElementsByClass(stream.Stream): # prepareStreamForMidi() supplies defaults for these if obj.getElementsByClass(('MetronomeMark', 'TimeSignature')): @@ -2741,11 +2766,11 @@ def streamHierarchyToMidiTracks( def midiTracksToStreams( midiTracks: list[MidiTrack], ticksPerQuarter: int = defaults.ticksPerQuarter, - quantizePost=True, + quantizePost: bool = True, inputM21: stream.Score|None = None, *, encoding: str = 'utf-8', - **keywords + **keywords: t.Any ) -> stream.Score: ''' Given a list of midiTracks, populate either a new stream.Score or inputM21 @@ -2841,12 +2866,12 @@ def streamToMidiFile( def midiFilePathToStream( - filePath, + filePath: str|pathlib.Path, *, - inputM21=None, + inputM21: stream.Score|None = None, encoding: str = 'utf-8', - **keywords, -): + **keywords: t.Any, +) -> stream.Score: ''' Used by music21.converter: @@ -2877,9 +2902,9 @@ def midiFilePathToStream( def midiAsciiStringToBinaryString( - midiFormat=1, - ticksPerQuarterNote=960, - tracksEventsList=None + midiFormat: int = 1, + ticksPerQuarterNote: int = 960, + tracksEventsList: list[list[str]]|None = None ) -> bytes: r''' Convert Ascii midi data to a bytes object (formerly binary midi string). @@ -2910,7 +2935,7 @@ def midiAsciiStringToBinaryString( ''' mf = MidiFile() - numTracks = len(tracksEventsList) + numTracks = len(tracksEventsList) if tracksEventsList is not None else 0 if numTracks == 1: mf.format = 1 @@ -2960,7 +2985,7 @@ def midiAsciiStringToBinaryString( return midiBinStr -def midiStringToStream(strData, **keywords): +def midiStringToStream(strData: bytes, **keywords: t.Any) -> stream.Score: r''' Convert a string of binary midi data to a Music21 stream.Score object. @@ -2994,11 +3019,11 @@ def midiStringToStream(strData, **keywords): def midiFileToStream( mf: MidiFile, *, - inputM21=None, - quantizePost=True, + inputM21: stream.Score|None = None, + quantizePost: bool = True, encoding: str = 'utf-8', - **keywords -): + **keywords: t.Any +) -> stream.Score: # noinspection PyShadowingNames ''' Note: this is NOT the normal way to read a MIDI file. The best way is generally: From b4bc21d1cd851c78049a1ede6fca6ef1955cd070 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 16 Jun 2026 17:37:27 -1000 Subject: [PATCH 2/3] midi: use t.cast for type narrowing instead of TYPE_CHECKING+assert Convert the `if t.TYPE_CHECKING: assert isinstance(...)` narrowings added in the previous commit to the preferred `t.cast(...)`-to-new-variable form (percussion.py: 2 spots; translate.py: 4 spots). This narrows for both mypy and ty (the assert form did not persist across re-reads of a @property under ty), and is the style the project is standardizing on. mypy and ruff clean; tests.py and doctests pass. AI-assisted (Claude) --- music21/midi/percussion.py | 16 +++++++--------- music21/midi/translate.py | 19 +++++++------------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/music21/midi/percussion.py b/music21/midi/percussion.py index 6e580ed559..3cb11ec5eb 100644 --- a/music21/midi/percussion.py +++ b/music21/midi/percussion.py @@ -158,11 +158,11 @@ def midiPitchToInstrument(self, midiPitch: int|pitch.Pitch) -> instrument.Instru midiInstrumentObject = midiInstrument() if (midiInstrumentObject.inGMPercMap is True and hasattr(midiInstrumentObject, '_percMapPitchToModifier')): - if t.TYPE_CHECKING: - assert isinstance(midiInstrumentObject, instrument.UnpitchedPercussion) - if midiNumber in midiInstrumentObject._percMapPitchToModifier: - modifier = midiInstrumentObject._percMapPitchToModifier[midiNumber] - midiInstrumentObject.modifier = modifier + midiUnpitchedPercussion = t.cast( + instrument.UnpitchedPercussion, midiInstrumentObject) + if midiNumber in midiUnpitchedPercussion._percMapPitchToModifier: + modifier = midiUnpitchedPercussion._percMapPitchToModifier[midiNumber] + midiUnpitchedPercussion.modifier = modifier return midiInstrumentObject @@ -194,10 +194,8 @@ def midiInstrumentToPitch(self, midiInstrument: instrument.Instrument) -> pitch. ''' if not hasattr(midiInstrument, 'inGMPercMap') or midiInstrument.inGMPercMap is False: raise MIDIPercussionException(f'{midiInstrument!r} is not in the GM Percussion Map!') - if t.TYPE_CHECKING: - assert isinstance(midiInstrument, instrument.Percussion) - assert midiInstrument.percMapPitch is not None - midiPitch = midiInstrument.percMapPitch + percInstrument = t.cast(instrument.Percussion, midiInstrument) + midiPitch = t.cast(int, percInstrument.percMapPitch) pitchObject = pitch.Pitch() pitchObject.midi = midiPitch return pitchObject diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 687a35c5ed..b9b3101368 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -451,9 +451,7 @@ def midiEventsToNote( if isinstance(nr, note.Note): nr.pitch.midi = midiOnEvent.pitch elif isinstance(nr, note.Unpitched): - onPitch = midiOnEvent.pitch - if t.TYPE_CHECKING: - assert onPitch is not None + onPitch = t.cast(int, midiOnEvent.pitch) try: i = PERCUSSION_MAPPER.midiPitchToInstrument(onPitch) except MIDIPercussionException: @@ -1028,9 +1026,8 @@ def midiEventsToTimeSignature( event = eventList[1] # time signature is 4 byte encoding - if t.TYPE_CHECKING: - assert isinstance(event.data, bytes) - post = list(event.data) + eventData = t.cast(bytes, event.data) + post = list(eventData) n = post[0] d = pow(2, post[1]) @@ -1139,9 +1136,8 @@ def midiEventsToKey(eventList: MidiEvent|Sequence[MidiEvent]) -> key.Key: event = eventList else: # get the second event; first is delta time event = eventList[1] - if t.TYPE_CHECKING: - assert isinstance(event.data, bytes) - post = list(event.data) + eventData = t.cast(bytes, event.data) + post = list(eventData) # first value is number of sharp, or neg for number of flat if post[0] > 12: @@ -1235,9 +1231,8 @@ def midiEventsToTempo(eventList: MidiEvent|Sequence[MidiEvent]) -> tempo.Metrono else: # get the second event; first is delta time event = eventList[1] # get microseconds per quarter - if t.TYPE_CHECKING: - assert isinstance(event.data, bytes) - mspq = getNumber(event.data, 3)[0] # first data is number + eventData = t.cast(bytes, event.data) + mspq = getNumber(eventData, 3)[0] # first data is number bpm = round(60_000_000 / mspq, 2) # post = list(event.data) # environLocal.printDebug(['midiEventsToTempo, got bpm', bpm]) From 274e956c3b0602c2800e79e2215070952d29e071 Mon Sep 17 00:00:00 2001 From: Michael Scott Asato Cuthbert Date: Tue, 16 Jun 2026 17:49:11 -1000 Subject: [PATCH 3/3] midi.translate: drop now-unused `from music21 import common` import After merging master, the deprecated midiEventsToInstrument() (decorated with @common.deprecated('v10', 'v11', ...)) has been removed, so `common` was no longer referenced in real code -- only in doctests, which resolve it via the music21 package namespace, not this module's import. ruff F401 and pylint W0611 (CI lint/ruff jobs) flagged it on the PR's merge commit. AI-assisted (Claude) --- music21/midi/translate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/music21/midi/translate.py b/music21/midi/translate.py index 7840590430..867f9422ef 100644 --- a/music21/midi/translate.py +++ b/music21/midi/translate.py @@ -21,7 +21,6 @@ import warnings from music21 import chord -from music21 import common from music21.common.numberTools import opFrac from music21 import defaults from music21 import duration