Files
pytheory/tests/test_polish_fixes.py
kennethreitz dc07850082 Polish sweep: robustness fixes & friendlier errors; release 0.57.1
- Key.secondary_dominant() wraps out-of-range degrees instead of
  crashing with IndexError; rejects non-positive degrees with ValueError
- Score.from_midi() guards zero tempo (falls back to 120 BPM) and
  zero/negative ticks_per_beat
- Fretboard.chord() raises descriptive ValueError instead of bare KeyError
- Scale[...], scale registry, and TimeSignature.from_string() give
  helpful error messages
- TimeSignature is hashable again (__eq__ without __hash__)
- Docstrings for Tone.pitch() and Scale.degree(); type hint on
  Chord.negative_harmony()
- New tests/test_polish_fixes.py (7 regression tests)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 07:38:46 -04:00

61 lines
2.0 KiB
Python

"""Regression tests for the quality/DX polish sweep.
Each test pins a robustness or developer-experience fix so it can't
silently regress: friendly errors instead of bare exceptions, value-type
dunder contracts, malformed-input guards, and boundary handling.
"""
import pytest
from pytheory import Tone, Chord, Fretboard
from pytheory.scales import Key
from pytheory.rhythm import TimeSignature
# ── Key.secondary_dominant: no crash on out-of-range / wrapping degrees ──
def test_secondary_dominant_wraps_past_octave():
k = Key("C", "major")
# V/9 wraps to V/2 (degree 9 -> degree 2), no IndexError.
assert k.secondary_dominant(9).identify() == k.secondary_dominant(2).identify()
def test_secondary_dominant_rejects_nonpositive_degree():
k = Key("C", "major")
with pytest.raises(ValueError, match="positive integer"):
k.secondary_dominant(0)
# ── TimeSignature: hashable (defines __eq__, so must define __hash__) ──
def test_time_signature_hashable():
d = {TimeSignature(4, 4): "common", TimeSignature(6, 8): "compound"}
assert d[TimeSignature(4, 4)] == "common"
assert hash(TimeSignature(3, 4)) == hash(TimeSignature(3, 4))
def test_time_signature_from_string_friendly_error():
with pytest.raises(ValueError, match="Invalid time signature"):
TimeSignature.from_string("bogus")
# ── Friendly errors instead of bare exceptions ──
def test_fretboard_chord_unparseable_raises_value_error():
fb = Fretboard(tones=[Tone.from_string(n)
for n in ("E2", "A2", "D3", "G3", "B3", "E4")])
with pytest.raises(ValueError, match="chord symbol"):
fb.chord("NotAChord!")
def test_scale_getitem_unknown_degree_message():
scale = Key("C", "major").scale
with pytest.raises(KeyError, match="scale degree"):
scale["ZZ"]
# ── Tone.pitch stays correct (now documented) ──
def test_tone_pitch_reference():
assert Tone.from_string("A4").pitch() == 440.0
assert Tone.from_string("A4").pitch(reference_pitch=432.0) == 432.0