From f71f34c92b4e537aa67cd68fe045175bf2b3b7e4 Mon Sep 17 00:00:00 2001 From: Alexander Rodionov Date: Thu, 12 Sep 2024 11:49:30 +0300 Subject: [PATCH] Dev (#180) * add chromatic rotations for compatability * add empty intervalset * wip * Scale(name='empty') validation * add row and col data to svg polygons for isomporphic keyboard --- .pre-commit-config.yaml | 10 +++++----- pyproject.toml | 1 + src/musiclib/config.py | 5 ++--- src/musiclib/midi/parse.py | 2 +- src/musiclib/progression.py | 2 +- src/musiclib/scale.py | 2 ++ src/musiclib/svg/isomorphic/base.py | 4 ++++ src/musiclib/svg/pianoroll.py | 2 +- src/musiclib/voice_leading/checks.py | 12 ++++++++++-- tests/cache_test.py | 1 + tests/conftest.py | 1 + tests/interval_test.py | 1 + tests/intervalset_test.py | 1 + tests/midi/parse_test.py | 1 + tests/midi/pitchbend_test.py | 1 + tests/midi/player_test.py | 1 + tests/note_test.py | 1 + tests/noteset_test.py | 1 + tests/pickle_test.py | 1 + tests/pitch_test.py | 1 + tests/progression_test.py | 1 + tests/rhythm_test.py | 1 + tests/scale_test.py | 6 ++++++ tests/specificnoteset_test.py | 1 + tests/svg/isomorphic_test.py | 1 + tests/svg/piano_test.py | 1 + tests/svg/repr_svg_test.py | 1 + tests/tempo_test.py | 1 + tests/util/etc_test.py | 1 + tests/voice_leading/checks_test.py | 1 + tests/voice_leading/transition_test.py | 1 + 31 files changed, 54 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f970bcb..5cc36475 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: - id: add-trailing-comma - repo: https://github.com/asottile/pyupgrade - rev: v3.15.2 + rev: v3.17.0 hooks: - id: pyupgrade @@ -42,26 +42,26 @@ repos: - id: autoflake - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.4.7 + rev: v0.6.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/PyCQA/flake8 - rev: 7.0.0 + rev: 7.1.1 hooks: - id: flake8 additional_dependencies: [Flake8-pyproject, flake8-functions-names] - repo: https://github.com/PyCQA/pylint - rev: v3.0.3 + rev: v3.2.7 hooks: - id: pylint # additional_dependencies: ["pylint-per-file-ignores"] # https://github.com/christopherpickering/pylint-per-file-ignores/issues/76 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + rev: v1.11.2 hooks: - id: mypy additional_dependencies: diff --git a/pyproject.toml b/pyproject.toml index b0570ffa..57cb381e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,6 +137,7 @@ ignore = [ "PT011", "PT023", "N806", + "ICN001", ] [tool.ruff.lint.isort] diff --git a/src/musiclib/config.py b/src/musiclib/config.py index 354dfdbf..17053d00 100644 --- a/src/musiclib/config.py +++ b/src/musiclib/config.py @@ -15,9 +15,8 @@ name_to_intervals_kind_grouped = { - 'chromatic_kind': { - 'chromatic': {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, - }, + 'chromatic': named_intervals_rotations({0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, 'chromatic'), + 'emtykind': {'empty': set()}, 'natural': { 'major': {0, 2, 4, 5, 7, 9, 11}, 'dorian': {0, 2, 3, 5, 7, 9, 10}, diff --git a/src/musiclib/midi/parse.py b/src/musiclib/midi/parse.py index f8b339a0..58a9103e 100644 --- a/src/musiclib/midi/parse.py +++ b/src/musiclib/midi/parse.py @@ -149,7 +149,7 @@ def append_bar(noteset: SpecificNoteSet | None) -> None: nonlocal t for is_play in rhythm.notes: if is_play: - notes = [note__.i] if noteset is None else [note.i for note in noteset.notes] + notes = [note__.i] if noteset is None else [note.i for note in noteset.notes] # pylint: disable=possibly-used-before-assignment for i, note in enumerate(notes): track.append(mido.Message('note_on', note=note, velocity=100, time=t if i == 0 else 0)) for i, note in enumerate(notes): diff --git a/src/musiclib/progression.py b/src/musiclib/progression.py index 3a567f76..6801a008 100644 --- a/src/musiclib/progression.py +++ b/src/musiclib/progression.py @@ -67,7 +67,7 @@ def transpose_unique_key(self, *, origin_name: bool = True) -> tuple[frozenset[i origin = self[0][0] # pylint: disable=unsubscriptable-object key = tuple(frozenset(note - origin for note in chord.notes) for chord in self) if origin_name: - return origin.abstract.i, key + return origin.abstract.i, key # pylint: disable=no-member return key def __add__(self, other: int) -> Progression: diff --git a/src/musiclib/scale.py b/src/musiclib/scale.py index 945605b4..e08a23ab 100644 --- a/src/musiclib/scale.py +++ b/src/musiclib/scale.py @@ -41,6 +41,8 @@ def __init__(self, root: Note, intervalset: IntervalSet) -> None: @classmethod def from_name(cls: type[Self], root: str | Note, name: str) -> Self: + if name == 'empty': + raise ValueError("name 'empty' is not allowed for Scale, because Scale should have at least 1 note (root note)") if isinstance(root, str): root = Note(root) elif not isinstance(root, Note): diff --git a/src/musiclib/svg/isomorphic/base.py b/src/musiclib/svg/isomorphic/base.py index bd94b0c8..620bf47e 100644 --- a/src/musiclib/svg/isomorphic/base.py +++ b/src/musiclib/svg/isomorphic/base.py @@ -236,6 +236,8 @@ def add_key(self, row: float, col: float) -> None: polygon.interval = interval # type: ignore[attr-defined] polygon.abstract_interval = abstract_interval.interval # type: ignore[attr-defined] polygon.abstract_interval_base12 = abstract_interval # type: ignore[attr-defined] + polygon.row = row # type: ignore[attr-defined] + polygon.col = col # type: ignore[attr-defined] self.elements.append(polygon) if self.n_parts is not None: @@ -262,6 +264,8 @@ def add_key(self, row: float, col: float) -> None: polygon.interval = interval # type: ignore[attr-defined] polygon.abstract_interval = abstract_interval.interval # type: ignore[attr-defined] polygon.abstract_interval_base12 = abstract_interval # type: ignore[attr-defined] + polygon.row = row # type: ignore[attr-defined] + polygon.col = col # type: ignore[attr-defined] self.elements.append(polygon) @abc.abstractmethod diff --git a/src/musiclib/svg/pianoroll.py b/src/musiclib/svg/pianoroll.py index c1118595..fc6840e3 100644 --- a/src/musiclib/svg/pianoroll.py +++ b/src/musiclib/svg/pianoroll.py @@ -50,7 +50,7 @@ def __init__( self.piano = IsoPiano( n_cols=len(self.sns), interval_colors=dict.fromkeys(self.sns.intervals, config.WHITE_PALE), - interval_strokes=dict.fromkeys(self.sns.intervals, {'stroke': config.BLACK_PALE, 'stroke_width': 0.5}), + interval_strokes={i: {'stroke': config.BLACK_PALE, 'stroke_width': 0.5} for i in self.sns.intervals}, interval_text=FromIntervalDict({i: str(n) for i, n in zip(self.sns.intervals, self.sns.notes_ascending, strict=True)}), radius=key_width // 2, radius1=key_height // 2, diff --git a/src/musiclib/voice_leading/checks.py b/src/musiclib/voice_leading/checks.py index 9879273e..595ce10f 100644 --- a/src/musiclib/voice_leading/checks.py +++ b/src/musiclib/voice_leading/checks.py @@ -101,14 +101,22 @@ def is_make_major_scale_leading_tone_resolving_semitone_up( return tonic - leading_tone == 1 -def is_large_spacing(c: SpecificNoteSet, max_interval: int = 12, /) -> bool: +def is_large_spacing_intervals(c: tuple[int, ...], max_interval: int = 12, /) -> bool: return any(b - a > max_interval for a, b in itertools.pairwise(c)) -def is_small_spacing(c: SpecificNoteSet, min_interval: int = 3, /) -> bool: +def is_small_spacing_intervals(c: tuple[int, ...], min_interval: int = 3, /) -> bool: return any(b - a < min_interval for a, b in itertools.pairwise(c)) +def is_large_spacing(c: SpecificNoteSet, max_interval: int = 12, /) -> bool: + return is_large_spacing_intervals(c.intervals, max_interval) + + +def is_small_spacing(c: SpecificNoteSet, min_interval: int = 3, /) -> bool: + return is_small_spacing_intervals(c.intervals, min_interval) + + def find_paused_voices(a: SpecificNoteSet, b: SpecificNoteSet, n_notes: int) -> tuple[int, ...] | tuple[()]: if n_notes == 0 or len(a) == len(b) == 0: return () diff --git a/tests/cache_test.py b/tests/cache_test.py index 20164864..7d226ee1 100644 --- a/tests/cache_test.py +++ b/tests/cache_test.py @@ -1,6 +1,7 @@ import operator import pytest + from musiclib.interval import AbstractInterval from musiclib.intervalset import IntervalSet from musiclib.note import Note diff --git a/tests/conftest.py b/tests/conftest.py index 5a5560f7..1305878e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import mido import pytest + from musiclib.midi.parse import Midi from musiclib.midi.parse import MidiNote from musiclib.midi.parse import MidiPitch diff --git a/tests/interval_test.py b/tests/interval_test.py index 8af2d853..a6d69c8f 100644 --- a/tests/interval_test.py +++ b/tests/interval_test.py @@ -1,4 +1,5 @@ import pytest + from musiclib.interval import AbstractInterval diff --git a/tests/intervalset_test.py b/tests/intervalset_test.py index e9e6f79f..4ac1e440 100644 --- a/tests/intervalset_test.py +++ b/tests/intervalset_test.py @@ -1,4 +1,5 @@ import pytest + from musiclib.interval import AbstractInterval from musiclib.intervalset import IntervalSet diff --git a/tests/midi/parse_test.py b/tests/midi/parse_test.py index b4495c3b..ef9f3ae2 100644 --- a/tests/midi/parse_test.py +++ b/tests/midi/parse_test.py @@ -1,5 +1,6 @@ import mido import pytest + from musiclib.midi import parse from musiclib.note import SpecificNote from musiclib.noteset import SpecificNoteSet diff --git a/tests/midi/pitchbend_test.py b/tests/midi/pitchbend_test.py index a7940eb3..de0fcfe1 100644 --- a/tests/midi/pitchbend_test.py +++ b/tests/midi/pitchbend_test.py @@ -1,4 +1,5 @@ import pytest + from musiclib.midi import pitchbend from musiclib.midi.parse import Midi from musiclib.midi.parse import MidiNote diff --git a/tests/midi/player_test.py b/tests/midi/player_test.py index 6840af87..b4e462c8 100644 --- a/tests/midi/player_test.py +++ b/tests/midi/player_test.py @@ -1,4 +1,5 @@ import pytest + from musiclib.midi.player import Player from musiclib.note import SpecificNote from musiclib.noteset import SpecificNoteSet diff --git a/tests/note_test.py b/tests/note_test.py index 353b62c3..062f8ac2 100644 --- a/tests/note_test.py +++ b/tests/note_test.py @@ -3,6 +3,7 @@ import hypothesis.strategies as st import pytest from hypothesis import given + from musiclib.interval import AbstractInterval from musiclib.note import Note from musiclib.note import SpecificNote diff --git a/tests/noteset_test.py b/tests/noteset_test.py index 28474ed6..68e5229a 100644 --- a/tests/noteset_test.py +++ b/tests/noteset_test.py @@ -2,6 +2,7 @@ from collections.abc import Sequence import pytest + from musiclib import config from musiclib.interval import AbstractInterval from musiclib.note import Note diff --git a/tests/pickle_test.py b/tests/pickle_test.py index 318db850..6da3adc3 100644 --- a/tests/pickle_test.py +++ b/tests/pickle_test.py @@ -1,6 +1,7 @@ import pickle import pytest + from musiclib.interval import AbstractInterval from musiclib.intervalset import IntervalSet from musiclib.note import Note diff --git a/tests/pitch_test.py b/tests/pitch_test.py index 0856c305..98e565d2 100644 --- a/tests/pitch_test.py +++ b/tests/pitch_test.py @@ -1,4 +1,5 @@ import pytest + from musiclib.note import SpecificNote from musiclib.pitch import Pitch diff --git a/tests/progression_test.py b/tests/progression_test.py index fd598388..9d0a6c05 100644 --- a/tests/progression_test.py +++ b/tests/progression_test.py @@ -1,6 +1,7 @@ from collections.abc import Sequence import pytest + from musiclib.noteset import SpecificNoteSet from musiclib.progression import Progression diff --git a/tests/rhythm_test.py b/tests/rhythm_test.py index 34a5fc33..bb220ced 100644 --- a/tests/rhythm_test.py +++ b/tests/rhythm_test.py @@ -1,6 +1,7 @@ from collections import deque import pytest + from musiclib.rhythm import Rhythm diff --git a/tests/scale_test.py b/tests/scale_test.py index d3884c30..f424c009 100644 --- a/tests/scale_test.py +++ b/tests/scale_test.py @@ -1,6 +1,7 @@ import operator import pytest + from musiclib import config from musiclib.interval import AbstractInterval from musiclib.intervalset import IntervalSet @@ -82,6 +83,11 @@ def test_from_name(root, name, expected): assert Scale.from_name(root, name) == expected +def test_empty_validation(): + with pytest.raises(ValueError): + Scale.from_name(Note('C'), 'empty') + + @pytest.mark.parametrize( ('scale', 'note_to_interval'), [ (Scale.from_str('CDE/C'), {Note('C'): AbstractInterval(0), Note('D'): AbstractInterval(2), Note('E'): AbstractInterval(4)}), diff --git a/tests/specificnoteset_test.py b/tests/specificnoteset_test.py index 05935820..d78f700c 100644 --- a/tests/specificnoteset_test.py +++ b/tests/specificnoteset_test.py @@ -1,6 +1,7 @@ from collections.abc import Sequence import pytest + from musiclib import config from musiclib.note import SpecificNote from musiclib.noteset import NoteSet diff --git a/tests/svg/isomorphic_test.py b/tests/svg/isomorphic_test.py index 76d3454e..d2f4ff6d 100644 --- a/tests/svg/isomorphic_test.py +++ b/tests/svg/isomorphic_test.py @@ -1,4 +1,5 @@ import pytest + from musiclib.svg.isomorphic.hexagonal import Hexagonal from musiclib.svg.isomorphic.squared import Squared diff --git a/tests/svg/piano_test.py b/tests/svg/piano_test.py index e78c8e70..00ff6e42 100644 --- a/tests/svg/piano_test.py +++ b/tests/svg/piano_test.py @@ -3,6 +3,7 @@ import pytest from colortool import Color + from musiclib import config from musiclib.note import Note from musiclib.note import SpecificNote diff --git a/tests/svg/repr_svg_test.py b/tests/svg/repr_svg_test.py index df58f60a..8dcf69d6 100644 --- a/tests/svg/repr_svg_test.py +++ b/tests/svg/repr_svg_test.py @@ -2,6 +2,7 @@ import pytest from colortool import Color + from musiclib import config from musiclib.noteset import NoteSet from musiclib.noteset import SpecificNoteSet diff --git a/tests/tempo_test.py b/tests/tempo_test.py index 1bcf9c3d..edc5c083 100644 --- a/tests/tempo_test.py +++ b/tests/tempo_test.py @@ -1,6 +1,7 @@ import typing as tp import pytest + from musiclib.tempo import Tempo diff --git a/tests/util/etc_test.py b/tests/util/etc_test.py index a0f10d1c..8838d0f1 100644 --- a/tests/util/etc_test.py +++ b/tests/util/etc_test.py @@ -1,4 +1,5 @@ import pytest + from musiclib.util import etc diff --git a/tests/voice_leading/checks_test.py b/tests/voice_leading/checks_test.py index c4810d76..7970a470 100644 --- a/tests/voice_leading/checks_test.py +++ b/tests/voice_leading/checks_test.py @@ -1,4 +1,5 @@ import pytest + from musiclib.noteset import SpecificNoteSet from musiclib.voice_leading import checks diff --git a/tests/voice_leading/transition_test.py b/tests/voice_leading/transition_test.py index d623a7b1..f978c2d4 100644 --- a/tests/voice_leading/transition_test.py +++ b/tests/voice_leading/transition_test.py @@ -1,6 +1,7 @@ import itertools import pytest + from musiclib.note import SpecificNote from musiclib.noteset import NoteSet from musiclib.noteset import SpecificNoteSet