Fix scale_diagram enharmonic matching — v0.43.1

Scale notes spelled with flats (e.g. the Eb blue note in the blues
scale) were silently dropped from scale_diagram() because the fretboard
spells that pitch as D#, so the string comparison never matched. Match
notes enharmonically via the system's canonical name and display them
using the scale's own spelling. Pre-existing bug (also in 0.42.x),
surfaced while flipping the docs for the low-to-high release.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 11:44:46 -04:00
parent 41e8404624
commit 1ed90c72d6
7 changed files with 53 additions and 16 deletions
+8
View File
@@ -2,6 +2,14 @@
All notable changes to PyTheory are documented here. All notable changes to PyTheory are documented here.
## 0.43.1
- **Fix `Fretboard.scale_diagram()` enharmonic matching.** Scale notes
spelled with flats (e.g. the `Eb` blue note in the blues scale) were
silently omitted from the diagram, because the fretboard spells that
pitch as `D#`. Notes are now matched enharmonically (by pitch) and
displayed using the scale's own spelling.
## 0.43.0 ## 0.43.0
- **BREAKING — fingerings now read low-to-high by default.** `Fretboard` - **BREAKING — fingerings now read low-to-high by default.** `Fretboard`
+6 -6
View File
@@ -208,12 +208,12 @@ Visualize the blues scale on guitar to see the patterns:
>>> blues = TonedScale(tonic="A4", system="blues")["blues"] >>> blues = TonedScale(tonic="A4", system="blues")["blues"]
>>> print(fb.scale_diagram(blues, frets=12)) >>> print(fb.scale_diagram(blues, frets=12))
0 1 2 3 4 5 6 7 8 9 10 11 12 0 1 2 3 4 5 6 7 8 9 10 11 12
E| - | - | - | - | - | A | - | - | C | - | D | Eb| E | E| E | - | - | G | - | A | - | - | C | - | D | Eb| E |
A| A | - | - | C | - | D | Eb| E | - | - | - | - | A | A| A | - | - | C | - | D | Eb| E | - | - | G | - | A |
D| - | - | - | - | A | - | - | C | - | D | Eb| E | - | D| D | Eb| E | - | - | G | - | A | - | - | C | - | D |
G| - | - | A | - | - | C | - | D | Eb| E | - | - | - | G| G | - | A | - | - | C | - | D | Eb| E | - | - | G |
B| - | - | - | D | Eb| E | - | - | - | - | A | - | - | B| - | C | - | D | Eb| E | - | - | G | - | A | - | - |
E| - | - | - | - | - | A | - | - | C | - | D | Eb| E | E| E | - | - | G | - | A | - | - | C | - | D | Eb| E |
The minor pentatonic (same scale without the Eb) is the most-played The minor pentatonic (same scale without the Eb) is the most-played
scale in rock guitar. Add the blue note and you have the full blues scale in rock guitar. Add the blue note and you have the full blues
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "pytheory" name = "pytheory"
version = "0.43.0" version = "0.43.1"
description = "Music Theory for Humans" description = "Music Theory for Humans"
readme = "README.md" readme = "README.md"
license = "MIT" license = "MIT"
+1 -1
View File
@@ -1,6 +1,6 @@
"""PyTheory: Music Theory for Humans.""" """PyTheory: Music Theory for Humans."""
__version__ = "0.43.0" __version__ = "0.43.1"
from .tones import Tone, Interval from .tones import Tone, Interval
from .systems import System, SYSTEMS, TET from .systems import System, SYSTEMS, TET
+23 -7
View File
@@ -1867,10 +1867,24 @@ class Fretboard:
>>> am = Chord.from_symbol("Am") >>> am = Chord.from_symbol("Am")
>>> print(fb.scale_diagram(pentatonic, frets=5, chord=am)) >>> print(fb.scale_diagram(pentatonic, frets=5, chord=am))
""" """
scale_notes = set(scale.note_names) # Match notes enharmonically: the fretboard spells tones with
# sharps (e.g. D#), but a scale may use flats (e.g. Eb). Compare
# via the system's canonical name so Eb and D# count as the same
# pitch — and display using the scale's own spelling.
_system = self._tones[0].system
def _resolve(name):
resolved = _system.resolve_name(name)
return resolved if resolved is not None else name
# Map canonical pitch -> the scale's preferred spelling for display.
scale_display = {}
for n in scale.note_names:
scale_display.setdefault(_resolve(n), n)
scale_notes = set(scale_display)
chord_notes = set() chord_notes = set()
if chord is not None: if chord is not None:
chord_notes = {t.name for t in chord.tones} chord_notes = {_resolve(t.name) for t in chord.tones}
max_name = max(len(t.name) for t in self.tones) max_name = max(len(t.name) for t in self.tones)
lines = [] lines = []
@@ -1885,13 +1899,15 @@ class Fretboard:
fret_marks = [] fret_marks = []
for f in range(frets + 1): for f in range(frets + 1):
note = tone.add(f) note = tone.add(f)
if note.name in scale_notes: key = _resolve(note.name)
if chord_notes and note.name in chord_notes: if key in scale_notes:
fret_marks.append(f" {note.name.upper():<2s}") label = scale_display[key]
if chord_notes and key in chord_notes:
fret_marks.append(f" {label.upper():<2s}")
elif chord_notes: elif chord_notes:
fret_marks.append(f" {note.name.lower():<2s}") fret_marks.append(f" {label.lower():<2s}")
else: else:
fret_marks.append(f" {note.name:<2s}") fret_marks.append(f" {label:<2s}")
else: else:
fret_marks.append(" - ") fret_marks.append(" - ")
line = f"{tone.name:>{max_name}}|{'|'.join(fret_marks)}|" line = f"{tone.name:>{max_name}}|{'|'.join(fret_marks)}|"
+13
View File
@@ -3522,6 +3522,19 @@ def test_scale_diagram():
assert len(lines) == 7 assert len(lines) == 7
def test_scale_diagram_enharmonic_flat_note():
"""A flat-spelled scale note (e.g. the blues Eb) must render even
though the fretboard spells that pitch as D#."""
fb = Fretboard.guitar()
blues = TonedScale(tonic="A4", system="blues")["blues"]
assert "Eb" in blues.note_names
diagram = fb.scale_diagram(blues, frets=12)
# The blue note shows up using the scale's own (flat) spelling,
# never the fretboard's sharp spelling.
assert "Eb" in diagram
assert "D#" not in diagram
# ── Coverage gap tests ───────────────────────────────────────────────────── # ── Coverage gap tests ─────────────────────────────────────────────────────
def test_tone_init_octave_parsed_from_name(): def test_tone_init_octave_parsed_from_name():
Generated
+1 -1
View File
@@ -690,7 +690,7 @@ wheels = [
[[package]] [[package]]
name = "pytheory" name = "pytheory"
version = "0.43.0" version = "0.43.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "rich" }, { name = "rich" },