Drop numeral dependency, inline Roman numeral helpers

Replace the numeral package with ~30 lines of int2roman()/roman2int()
in _statics.py. Reduces supply chain surface. Fixes #47.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 17:48:22 -04:00
parent d3a93c18b3
commit 06fc4cabb7
5 changed files with 45 additions and 19 deletions
-1
View File
@@ -21,7 +21,6 @@ classifiers = [
"Programming Language :: Python :: 3.13",
]
dependencies = [
"numeral",
"sounddevice",
"scipy",
]
+37
View File
@@ -2,6 +2,43 @@ import math
REFERENCE_A = 440
# ── Roman numeral helpers (replaces `numeral` package) ───────────────────
_ROMAN_MAP = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
]
_ROMAN_VALUES = {
"I": 1, "V": 5, "X": 10, "L": 50, "C": 100, "D": 500, "M": 1000,
}
def int2roman(n: int) -> str:
"""Convert an integer to an uppercase Roman numeral string."""
result = []
for value, numeral in _ROMAN_MAP:
while n >= value:
result.append(numeral)
n -= value
return "".join(result)
def roman2int(s: str) -> int:
"""Convert a Roman numeral string (case-insensitive) to an integer."""
s = s.upper()
total = 0
prev = 0
for ch in reversed(s):
val = _ROMAN_VALUES.get(ch, 0)
if val < prev:
total -= val
else:
total += val
prev = val
return total
# Index of C in the Western tone list (A=0, A#=1, B=2, C=3, ...).
# Scientific pitch notation changes octave at C, not A, so this offset
# is needed for all octave arithmetic.
+2 -2
View File
@@ -849,7 +849,7 @@ class Chord:
>>> Chord([D4, F4, A4]).analyze("C")
'ii'
"""
import numeral as numeral_mod
from ._statics import int2roman
from .scales import TonedScale
from .systems import SYSTEMS
from .tones import Tone
@@ -874,7 +874,7 @@ class Chord:
scale_names = [t.name for t in scale.tones[:-1]]
def _build_numeral(root, quality, degree_idx, prefix=""):
numeral_str = numeral_mod.int2roman(degree_idx + 1, only_ascii=True)
numeral_str = int2roman(degree_idx + 1)
suffix = ""
if "minor" in quality:
numeral_str = numeral_str.lower()
+6 -6
View File
@@ -2,8 +2,6 @@ from __future__ import annotations
from typing import Optional, Union
import numeral
from .systems import SYSTEMS, System
from .tones import Tone
@@ -49,7 +47,8 @@ class Scale:
def __repr__(self) -> str:
r = []
for (i, tone) in enumerate(self.tones):
degree = numeral.int2roman(i + 1, only_ascii=True)
from ._statics import int2roman
degree = int2roman(i + 1)
r += [f"{degree}={tone.full_name}"]
r = " ".join(r)
@@ -200,7 +199,7 @@ class Scale:
>>> scale.progression("I", "IV", "V", "I")
[<Chord (C,E,G)>, <Chord (F,A,C)>, <Chord (G,B,D)>, <Chord (C,E,G)>]
"""
import numeral as numeral_mod
from ._statics import roman2int
chords = []
for num in numerals:
is_seventh = num.endswith("7")
@@ -213,7 +212,7 @@ class Scale:
elif clean.startswith("#") and len(clean) > 1:
clean = clean[1:]
flat_offset = 1 # one semitone up
degree = numeral_mod.roman2int(clean.upper()) - 1
degree = roman2int(clean.upper()) - 1
if is_seventh:
chord = self.seventh(degree)
else:
@@ -406,7 +405,8 @@ class Scale:
if isinstance(item, str):
degrees = []
for (i, tone) in enumerate(self.tones):
degrees.append(numeral.int2roman(i + 1, only_ascii=True))
from ._statics import int2roman
degrees.append(int2roman(i + 1))
if item in degrees:
item = degrees.index(item)
Generated
-10
View File
@@ -486,14 +486,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" },
]
[[package]]
name = "numeral"
version = "0.1.0.17"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/0d/ac6a186e169fcbdfea316f78fb5e34981bcf8d5c1d7cc8b6581f597e1e4c/numeral-0.1.0.17-py2.py3-none-any.whl", hash = "sha256:7dff0c1efb9b3655c9c1dc93b4666993741b15abcac0dc01dcb96b21cc20f6ae", size = 22066, upload-time = "2020-04-12T08:24:59.129Z" },
]
[[package]]
name = "numpy"
version = "2.2.6"
@@ -701,7 +693,6 @@ name = "pytheory"
version = "0.38.2"
source = { editable = "." }
dependencies = [
{ name = "numeral" },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "sounddevice" },
@@ -721,7 +712,6 @@ docs = [
[package.metadata]
requires-dist = [
{ name = "numeral" },
{ name = "scipy" },
{ name = "sounddevice" },
]