diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 53b9754..71a7510 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] os: [ ubuntu-latest, macOS-latest, windows-latest ] steps: diff --git a/miditoolkit/constants.py b/miditoolkit/constants.py index 56b3a02..475db91 100644 --- a/miditoolkit/constants.py +++ b/miditoolkit/constants.py @@ -21,4 +21,31 @@ MAJOR_NAMES = ["M", "Maj", "Major", "maj", "major"] MINOR_NAMES = ["m", "Min", "Minor", "min", "minor"] +KEY_NUMBER_TO_MIDO_KEY_NAME = [ + "C", + "Db", + "D", + "Eb", + "E", + "F", + "F#", + "G", + "Ab", + "A", + "Bb", + "B", + "Cm", + "C#m", + "Dm", + "D#m", + "Em", + "Fm", + "F#m", + "Gm", + "G#m", + "Am", + "Bbm", + "Bm", +] + DEFAULT_BPM = 120 diff --git a/miditoolkit/midi/containers.py b/miditoolkit/midi/containers.py index 93f36f9..8e0f75b 100755 --- a/miditoolkit/midi/containers.py +++ b/miditoolkit/midi/containers.py @@ -1,7 +1,8 @@ +from __future__ import annotations + import re import warnings from dataclasses import dataclass -from typing import List, Optional, Union from ..constants import MAJOR_NAMES, MINOR_NAMES @@ -232,7 +233,7 @@ class TempoChange: """ - tempo: Union[float, int] + tempo: float | int time: int def __str__(self): @@ -273,10 +274,10 @@ def __init__( program: int, is_drum: bool = False, name: str = "", - notes: Optional[List[Note]] = None, - pitch_bends: Optional[List[PitchBend]] = None, - control_changes: Optional[List[ControlChange]] = None, - pedals: Optional[List[Pedal]] = None, + notes: list[Note] | None = None, + pitch_bends: list[PitchBend] | None = None, + control_changes: list[ControlChange] | None = None, + pedals: list[Pedal] | None = None, ): """Create the Instrument.""" self.program = program @@ -327,7 +328,9 @@ def __eq__(self, other): return False if any( a1 != a2 - for a1, a2 in zip(getattr(self, list_attr), getattr(other, list_attr)) + for a1, a2 in zip( + getattr(self, list_attr), getattr(other, list_attr), strict=False + ) ): return False diff --git a/miditoolkit/midi/parser.py b/miditoolkit/midi/parser.py index b674264..da467f0 100755 --- a/miditoolkit/midi/parser.py +++ b/miditoolkit/midi/parser.py @@ -1,12 +1,14 @@ +from __future__ import annotations + import collections import functools +from collections.abc import Sequence from pathlib import Path -from typing import List, Optional, Sequence, Tuple, Union import mido import numpy as np -from ..constants import DEFAULT_BPM +from ..constants import DEFAULT_BPM, KEY_NUMBER_TO_MIDO_KEY_NAME from .containers import ( ControlChange, Instrument, @@ -30,7 +32,7 @@ class MidiFile: def __init__( self, - filename: Optional[Union[Path, str]] = None, + filename: Path | str | None = None, file=None, ticks_per_beat: int = 480, clip: bool = False, @@ -39,12 +41,12 @@ def __init__( # create empty file self.ticks_per_beat: int = ticks_per_beat self.max_tick: int = 0 - self.tempo_changes: List[TempoChange] = [] - self.time_signature_changes: List[TimeSignature] = [] - self.key_signature_changes: List[KeySignature] = [] - self.lyrics: List[Lyric] = [] - self.markers: List[Marker] = [] - self.instruments: List[Instrument] = [] + self.tempo_changes: list[TempoChange] = [] + self.time_signature_changes: list[TimeSignature] = [] + self.key_signature_changes: list[KeySignature] = [] + self.lyrics: list[Lyric] = [] + self.markers: list[Marker] = [] + self.instruments: list[Instrument] = [] # load file if filename or file: @@ -96,7 +98,7 @@ def _convert_delta_to_cumulative(mido_obj: mido.MidiFile): tick = event.time @staticmethod - def _load_tempo_changes(mido_obj: mido.MidiFile) -> List[TempoChange]: + def _load_tempo_changes(mido_obj: mido.MidiFile) -> list[TempoChange]: # default bpm tempo_changes = [TempoChange(DEFAULT_BPM, 0)] @@ -116,7 +118,7 @@ def _load_tempo_changes(mido_obj: mido.MidiFile) -> List[TempoChange]: return tempo_changes @staticmethod - def _load_time_signatures(mido_obj: mido.MidiFile) -> List[TimeSignature]: + def _load_time_signatures(mido_obj: mido.MidiFile) -> list[TimeSignature]: # no default time_signature_changes = [] @@ -131,7 +133,7 @@ def _load_time_signatures(mido_obj: mido.MidiFile) -> List[TimeSignature]: return time_signature_changes @staticmethod - def _load_key_signatures(mido_obj: mido.MidiFile) -> List[KeySignature]: + def _load_key_signatures(mido_obj: mido.MidiFile) -> list[KeySignature]: # no default key_signature_changes = [] @@ -144,7 +146,7 @@ def _load_key_signatures(mido_obj: mido.MidiFile) -> List[KeySignature]: return key_signature_changes @staticmethod - def _load_markers(mido_obj: mido.MidiFile) -> List[Marker]: + def _load_markers(mido_obj: mido.MidiFile) -> list[Marker]: # no default markers = [] @@ -156,7 +158,7 @@ def _load_markers(mido_obj: mido.MidiFile) -> List[Marker]: return markers @staticmethod - def _load_lyrics(mido_obj: mido.MidiFile) -> List[Lyric]: + def _load_lyrics(mido_obj: mido.MidiFile) -> list[Lyric]: # no default lyrics = [] @@ -168,7 +170,7 @@ def _load_lyrics(mido_obj: mido.MidiFile) -> List[Lyric]: return lyrics @staticmethod - def _load_instruments(midi_data: mido.MidiFile) -> List[Instrument]: + def _load_instruments(midi_data: mido.MidiFile) -> list[Instrument]: instrument_map = collections.OrderedDict() # Store a similar mapping to instruments storing "straggler events", # e.g. events which appear before we want to initialize an Instrument @@ -377,7 +379,9 @@ def __eq__(self, other): return False if any( a1 != a2 - for a1, a2 in zip(getattr(self, list_attr), getattr(other, list_attr)) + for a1, a2 in zip( + getattr(self, list_attr), getattr(other, list_attr), strict=False + ) ): return False @@ -386,11 +390,11 @@ def __eq__(self, other): def dump( self, - filename: Optional[Union[str, Path]] = None, + filename: str | Path | None = None, file=None, - segment: Optional[Tuple[int, int]] = None, + segment: tuple[int, int] | None = None, shift: bool = True, - instrument_idx: Optional[int] = None, + instrument_idx: int | None = None, charset: str = "latin1", ): # comparison function @@ -499,7 +503,11 @@ def event_compare(event1, event2): key_list = [] for ks in self.key_signature_changes: key_list.append( - mido.MetaMessage("key_signature", time=ks.time, key=ks.key_name) + mido.MetaMessage( + "key_signature", + time=ks.time, + key=KEY_NUMBER_TO_MIDO_KEY_NAME[ks.key_number], + ) ) # crop segment @@ -704,7 +712,7 @@ def _is_note_within_tick_range( def _include_meta_events_within_tick_range( - events: Sequence[Union[mido.MetaMessage, mido.Message]], + events: Sequence[mido.MetaMessage | mido.Message], start_tick: int, end_tick: int, shift: bool = False, @@ -757,7 +765,7 @@ def _include_meta_events_within_tick_range( return proc_events -def _get_tick_eq_of_second(sec: Union[float, int], tick_to_time: np.ndarray) -> int: +def _get_tick_eq_of_second(sec: float | int, tick_to_time: np.ndarray) -> int: return int((np.abs(tick_to_time - sec)).argmin()) diff --git a/miditoolkit/pianoroll/parser.py b/miditoolkit/pianoroll/parser.py index 7c566ba..3db3f4c 100755 --- a/miditoolkit/pianoroll/parser.py +++ b/miditoolkit/pianoroll/parser.py @@ -1,5 +1,7 @@ +from __future__ import annotations + +from collections.abc import Callable from copy import deepcopy -from typing import Callable, List, Optional, Tuple, Union import numpy as np @@ -8,13 +10,13 @@ def notes2pianoroll( - notes: List[Note], - pitch_range: Optional[Tuple[int, int]] = None, + notes: list[Note], + pitch_range: tuple[int, int] | None = None, pitch_offset: int = 0, - resample_factor: Optional[float] = None, + resample_factor: float | None = None, resample_method: Callable = round, velocity_threshold: int = 0, - time_portion: Optional[Tuple[int, int]] = None, + time_portion: tuple[int, int] | None = None, keep_note_with_zero_duration: bool = True, ) -> np.ndarray: r"""Converts a sequence of notes into a pianoroll numpy array. @@ -120,9 +122,9 @@ def notes2pianoroll( def pianoroll2notes( pianoroll: np.ndarray, - resample_factor: Optional[float] = None, - pitch_range: Optional[Union[int, Tuple[int, int]]] = None, -) -> List[Note]: + resample_factor: float | None = None, + pitch_range: int | tuple[int, int] | None = None, +) -> list[Note]: """Converts a pianoroll (numpy array) into a sequence of notes. Args: diff --git a/miditoolkit/pianoroll/vis.py b/miditoolkit/pianoroll/vis.py index 7d02f87..a32d75d 100644 --- a/miditoolkit/pianoroll/vis.py +++ b/miditoolkit/pianoroll/vis.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple, Union +from __future__ import annotations import numpy as np from matplotlib import pyplot as plt @@ -34,20 +34,20 @@ # -------------------------------------------- # def plot( pianoroll: np.ndarray, - note_range: Tuple[int, int] = (0, 128), + note_range: tuple[int, int] = (0, 128), beat_resolution: int = 24, - downbeats: Union[int, np.ndarray] = 4, + downbeats: int | np.ndarray = 4, background_layout: str = "pianoroll", grid_layout: str = "x", xtick: str = "downbeat", ytick: str = "number", ytick_interval: int = 12, xtick_interval: int = 1, - x_range: Optional[Tuple[int, int]] = None, - y_range: Optional[Tuple[int, int]] = None, - figsize: Optional[Tuple[int, int]] = None, + x_range: tuple[int, int] | None = None, + y_range: tuple[int, int] | None = None, + figsize: tuple[int, int] | None = None, dpi: int = 300, -) -> Tuple[plt.Figure, plt.Axes]: +) -> tuple[plt.Figure, plt.Axes]: """Plot Pianoroll Parameters ---------- @@ -133,14 +133,14 @@ def plot( def plot_chroma( chroma: np.ndarray, beat_resolution: int = 24, - downbeats: Union[int, np.ndarray] = 4, + downbeats: int | np.ndarray = 4, xtick: str = "downbeat", ytick: str = "note", - x_range: Optional[Tuple[int, int]] = None, + x_range: tuple[int, int] | None = None, xtick_interval: int = 1, - figsize: Optional[Tuple[int, int]] = None, + figsize: tuple[int, int] | None = None, dpi: int = 300, -) -> Tuple[plt.Figure, plt.Axes]: +) -> tuple[plt.Figure, plt.Axes]: """Plot Chromagram Parameters ---------- @@ -217,11 +217,11 @@ def plot_chroma( def plot_heatmap( to_plot: np.ndarray, - tick_interval: Optional[int] = None, + tick_interval: int | None = None, origin: str = "upper", - figsize: Optional[Tuple[int, int]] = None, + figsize: tuple[int, int] | None = None, dpi: int = 300, -) -> Tuple[plt.Figure, plt.Axes]: +) -> tuple[plt.Figure, plt.Axes]: """Plot Similarity Matrix Parameters ---------- @@ -282,7 +282,7 @@ def plot_grid(ax: plt.Axes, layout: str, which: str = "minor", color="k"): # always using 'minor' tick to plot grid # argumens check if layout not in ["x", "y", "both", None]: - raise ValueError("Unkown Grid layout: %s" % layout) + raise ValueError(f"Unkown Grid layout: {layout}") # grid Show if layout in ["x", "both"]: @@ -332,7 +332,7 @@ def plot_xticks( xtick_interval: int, max_tick: int, beat_resolution: int, - downbeats: Optional[int] = None, + downbeats: int | None = None, ): # tick arrangement # - xtick, minor for beat @@ -352,7 +352,7 @@ def plot_xticks( elif downbeats.dtype == bool: xticks_downbeats = np.where(downbeats == True) # noqa: E712 else: - raise ValueError("Unkown downbeats type: %s" % downbeats) + raise ValueError(f"Unkown downbeats type: {downbeats}") ax.set_xticks(xticks_downbeats) ax.grid( axis="x", color="k", which="major", linestyle="-", linewidth=0.5, alpha=1.0 @@ -385,7 +385,7 @@ def plot_xticks( ax.tick_params(axis="x", which="minor", width=0) ax.tick_params(axis="x", which="major", width=0) else: - raise ValueError("Unkown xtick type: %s" % xtick) + raise ValueError(f"Unkown xtick type: {xtick}") def plot_background(ax: plt.Axes, layout: str, canvas): @@ -409,7 +409,7 @@ def plot_background(ax: plt.Axes, layout: str, canvas): elif layout == "blank": pass else: - raise ValueError("Unkown background layout: %s" % layout) + raise ValueError(f"Unkown background layout: {layout}") def plot_note_entries(ax: plt.Axes, to_plot): diff --git a/pyproject.toml b/pyproject.toml index 1cc5986..4f8421d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,3 +51,21 @@ path = "miditoolkit/__init__.py" include = [ "/miditoolkit", ] + + +[tool.ruff] +target-version = "py312" + +[tool.ruff.lint] +extend-select = [ + "B", + "ERA", + "I", + "PERF", + "RUF013", + "T", + "UP", +] + +[tool.ruff.per-file-ignores] +"miditoolkit/midi/parser.py" = ["PERF401"] diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index b85bc25..0000000 --- a/ruff.toml +++ /dev/null @@ -1,13 +0,0 @@ -target-version = "py37" -extend-select = [ - "B", - "ERA", - "I", - "PERF", - "RUF013", - "T", - "UP", -] - -[extend-per-file-ignores] -"miditoolkit/midi/parser.py" = ["PERF401"] diff --git a/tests/test_pianoroll.py b/tests/test_pianoroll.py index f59129e..9ba10c4 100644 --- a/tests/test_pianoroll.py +++ b/tests/test_pianoroll.py @@ -57,7 +57,7 @@ def test_pianoroll(midi_path, test_set, disable_mido_checks, disable_mido_merge_ assert len(new_notes) == len( new_new_notes ), "Number of notes changed in pianoroll conversion" - for note1, note2 in zip(new_notes, new_new_notes): + for note1, note2 in zip(new_notes, new_new_notes, strict=False): # We don't test the resampling factor as it might later the number of notes assert ( note1 == note2 diff --git a/tests/test_read_dump.py b/tests/test_read_dump.py index 2e17f30..0790a6e 100644 --- a/tests/test_read_dump.py +++ b/tests/test_read_dump.py @@ -15,7 +15,7 @@ def test_load_dump(midi_path, tmp_path, disable_mido_checks, disable_mido_merge_ midi2 = MidiFile(dump_path) # Loading it back # Sorting the notes, as after dump the order might have changed - for track1, track2 in zip(midi1.instruments, midi2.instruments): + for track1, track2 in zip(midi1.instruments, midi2.instruments, strict=False): track1.notes.sort(key=lambda x: (x.start, x.pitch, x.end, x.velocity)) track2.notes.sort(key=lambda x: (x.start, x.pitch, x.end, x.velocity))