Skip to content

Merge master into meter-speed-2026, trim docs, fix slow paths - #2199

Closed
float3 wants to merge 507 commits into
cuthbertLab:meter-speed-2026from
float3:help/meter-speed-2026
Closed

float3 wants to merge 507 commits into
cuthbertLab:meter-speed-2026from
float3:help/meter-speed-2026

Conversation

@float3

@float3 float3 commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

For #1972.

  • Merge master. Conflicts: Duration.__eq__ (kept master's linked and expressionIsInferred checks with this branch's isinstance and isGrace checks), version (11.0.0b16, since pickles of the old MeterSequence no longer load), AGENTS.md, and two Changed in lines in meter/core.py. Two OffsetQL annotations for mypy.
  • Docs: shorter, and say what the code does now. One commit per file: meter/core.py, meter/base.py, duration.py, base.py, tests.
  • MeterCore._reprInternal never ran (ProtoM21Object is first in the MRO); removed.
  • summedNumerator setter uses modify() instead of makeSequence and _partition.
  • _setDefaultAccentWeights and subdivideNestedHierarchy called setIndex once per accent or beat, which copied and hashed the whole partition each time and cached every step: O(n^2). Each now rebuilds once.
  • FrozenObject._check_init walks f_back instead of inspect.stack(), which read source files on every attribute set in FrozenDuration.__init__. The first TimeSignature of a meter goes from about 30 ms to 6 ms; music21/meter tests from 11 s to 1 s.

AI-assisted (Claude)

mscuthbert and others added 30 commits October 9, 2026 17:04
The in-place copy-back took the transposed copy's microtone only when it
was not None, so a microtone the transposition removed stayed:
C4(+50c) up a ChromaticInterval(0.5) in place gave C#4(+50c), while the
transposed copy is C#4.

AI-assisted (Claude)
Faster copies of slotted objects, Accidental, and Pitch
Find the next pitch of a cyclical scale where it is
…place-microtone

fix Pitch.transpose(inPlace) was keeping extra microtones
…aves

Fix caching problem / remove ref to defaultOctave
_parseName is now _cacheParsedName: it only parses and remembers (step,
accidental string, octave).  The .name setter reads the cache after a
miss too, so it sets the attributes and informs the Note once even the
first time, and a name that fails to parse leaves the pitch unchanged
(before, D$ changed the step to D and then raised).  Names over 100
characters raise a ValueError, so the cache cannot hold huge strings.

setStyleAttributes returns early for a tag with no attributes (most
<accidental>, <stem>, <type> tags): <accidental>natural</accidental>
1378 -> 491 ns; about 1-3% on whole MusicXML files.

AI-assisted (Claude)
Cache parsed Pitch names; Speedup MusicMXL Style parsing
romanNumeralFromChord(..., preferSecondaryDominants=True) relabelled
chords that belong to the key, although the docstring says only
chromatically altered chords are changed: I in major became V/IV,
III in minor V/VI, and IV in minor V/VII.

The substitution now only runs when the chord has a pitch outside the
key's scale. In minor, the raised sixth and seventh count as diatonic.

Fixes cuthbertLab#2053

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ings

Compare each pitch with the key signature instead of building the
scale with keyObj.pitches and transposing the sixth and seventh;
same results, about 100 times faster.

The test now checks every chord with and without
preferSecondaryDominants, including two chords whose figure changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atonic-check

(AI) preferSecondaryDominants: skip chords diatonic to the key
…-octave-change

(AI) Read MusicXML octave-change in transpose without diatonic
allParts() sorted the parts by their instrument's partId, which raised a
TypeError when the parts had no partId (as with MIDI imports) and put
P10 before P2. Use the order of the parts in the source score instead,
falling back to partId for parts that are not in the source.

Fixes cuthbertLab#2074
…le-keysig

(AI) Braille: read a lone key signature in a keyboard segment heading
A trailing blank or tab-only line in a DCML TSV file, such as one left
by copy-pasting a table from GitHub, raised a bare IndexError (or a
ValueError from int('')) in populateFromRow. Padded cells from the same
copy-paste left every header name unmatched, so the file then failed in
key.Key(''). _importTsv now strips each cell and drops rows with no
content.

A file with no chord rows after the header crashed in prepStream at
chordList[0]; it now raises TsvException, a Music21Exception.

Fixes cuthbertLab#1541
…order

(AI) Order TimespanTree.allParts() by the source score
A library should not pin its dependency tree; CI now resolves fresh from
pyproject.toml (uv sync without --locked).

AI-assisted (Claude)
- Pin ruff 0.17.1, mypy 2.4.0, pylint 4.1.3 so CI is stable without uv.lock.
- mypy 2 renamed the old allow_redefinition behavior to allow_redefinition_old;
  the new flexible mode caused 28 spurious errors.
- Levenshtein now ships types and ignores isjunk; pass None.
- Remove placeholder tests flagged by pylint's redundant-unittest-assert.

AI-assisted (Claude)
- Two doctests matched error text that 3.15 rewords; loosen with ellipses.
- actions/checkout and setup-python v7 (Node 20 deprecation warnings).
- Only run_tests saves the uv cache, avoiding the reserve-cache race.

AI-assisted (Claude)
coverage.py uses sys.monitoring by default on 3.14+, far faster than the
3.13 settrace core (3.13 coverage job took ~9 min vs ~3 for others).

AI-assisted (Claude)
Written by an AI agent (Chris, raisingchris2026), working alone.
…rline-segno

(AI) Read segno and coda inside a MusicXML barline
float3 and others added 23 commits November 1, 2026 10:24
The numeral was matched at the start of the figure and then every I,
V, i, v or N anywhere in it was deleted, so a numeral past seven read
as a smaller one with no error:

    roman.RomanNumeral('VIII', 'C').romanNumeral  # 'VII'
    roman.RomanNumeral('IX', 'C').pitches          # a C major triad

Only the matched numeral is removed now, and a numeral followed by
another numeral letter raises RomanNumeralException, worded like other
invalid figures.

The Monteverdi madrigal 5.1 analysis wrote VIII for the C major chord
in d minor at m. 28; it is VII, which is also how it was read.
…nstruments-2

Deduplicate instruments offset by offset
…asure-padding-2

# Conflicts:
#	music21/braille/segment.py
Like the first-staff test, it compares each measure's list of staff
numbers in one assertion, so a failure shows which measure lost which
staff.
A very long note or a high measure number in a small file made music21
build one measure per bar without limit, until it ran out of memory:

- a 37-byte MIDI file with one note of 2**28 ticks at 1 tick per
  quarter (converter.parse runs makeMeasures),
- MusicXML with <duration>99999999</duration> at 1 division
  (write() runs makeTies, which appends measures for the overflow),
- RomanText "Time Signature: 4/4\nm1 C: I\nm99999999 V"
  (the skipped measures are filled one by one).

makeMeasures, makeTies and the RomanText measure filler now raise
StreamException / RomanTextTranslateException once a part would have
more than defaults.maxMeasures (100,000) measures. makeMeasures and
makeTies check a lower bound (remaining length over the longest bar in
the meter stream) before each measure, so a single long note fails at
once and no input makes more than the limit. The longest corpus piece
has about 1,100 measures.
…gap-2

Join PartStaffs after a measure missing from the first staff
Stop making more than 100,000 measures from malformed input
…ure-padding-2

Keep an explicit zero quarterLength in GeneralNote
misc cleanup changes that didn't make it into the recent clef bug fix
Conflicts:
- AGENTS.md: kept master's testing rules and the branch's note on
  preexisting failures.
- _version.py, base.py: took master's 11.0.0b15.
- duration.py Duration.__eq__: kept master's expressionIsInferred docs and
  linked/expressionIsInferred checks with the branch's isinstance and
  isGrace checks; both Changed-in markers; both new tests.
- meter/core.py: kept the branch's Changed-in markers without master's
  dropped trailing blank line; kept the branch's removal of
  _subdivideNested.

Annotate qPos in offsetToIndex and pos in getAccent as OffsetQL: mypy
failed on them once duration became a FrozenDuration.
MeterTerminal and MeterSequence are slotted and immutable on this branch, so
streams pickled by 11.0.0b15 cannot be loaded.  The branch bumped b8 to b9
for the same reason before master moved past it.
Say what the code does in fewer words; drop history and comments that
restate the code.  Fix claims that no longer hold: weight may be an int,
subdivide is not the only method that returns a new object, and
getLevelWeight has no _weight to mention.  Add doctests for subdivide,
makeSequence and a withWeight example with unequal parts.
Shorten the cache and load comments, the barDuration note and the
getBeatDepth example text.  beatDuration and beatDivisionDurations name
FrozenDuration in their first line, matching what they return.
Shorten the Duration.__eq__ text and the FrozenDuration unfreeze,
augmentOrDiminish, splitDotGroups and __copy__ docs and comments.
Shorten the beatDuration unfreeze example text and the FrozenDuration
comments in the duration setter.
Drop comments that restate the assertion below them; shorten the
testAssignFrozenDurationStaysFrozen docstring.
ProtoM21Object comes before MeterCore in the MRO of both subclasses, and
each subclass defines its own _reprInternal, so this one never ran.
The setter rebuilt the display sequence through makeSequence and the
private _partition; modify(summedNumerator=value) does the same through
the public method.
_setDefaultAccentWeights called setIndex once per accent.  Each call
copied and hashed the whole partition and left an intermediate sequence
in _meterSequenceCache, so a meter with n accents did O(n^2) work and
cached n sequences that nothing uses.  Build the weighted terminals in a
list and call modify(partition=...) once.

Accent, beat and beam sequences for 153 meters match master (apart from
the int/float default weight this branch already changes).
inspect.stack() reads the source file and line for every frame, which
made each attribute set inside FrozenDuration.__init__ take milliseconds.
With MeterTerminal.duration returning a FrozenDuration, the first
TimeSignature of each meter took about 30 ms here (master: 1.5 ms).
Following f_back finds the same __init__/__new__/__setstate__ frame
without reading files: about 6 ms, and music21/meter tests run in 2 s
instead of 11 s.
_subdivideNestedByPaths replaced one path at a time through setIndex, so
each replacement copied and hashed every sequence above it, and cached
each intermediate root.  For n beats that is O(n^2) per level.  Collect
the replacements and rebuild only the sequences above them, once.

subdivideNestedHierarchy gives the same sequences and weights for 108
cases (12 meters, depths 1-4, both normalizeDenominators settings, and a
beatSequence as firstPartitionForm).
@coveralls

coveralls commented Nov 2, 2026 •

Copy link
Copy Markdown

Coverage Status

Coverage is 93.754% — float3:help/meter-speed-2026 into cuthbertLab:meter-speed-2026. No base build found for cuthbertLab:meter-speed-2026.

partition, partitionByCount, partitionByList, partitionByOtherMeterSequence,
subdividePartitionsEqual, subdivideNestedHierarchy, and setLevelWeight
changed the sequence in place before v11.  Under the old names, a call such
as ts.beatSequence.partition(6) now did nothing.  The methods are now
partitioned, partitionedByCount, partitionedByList,
partitionedByOtherMeterSequence, subdividedPartitionsEqual,
subdividedNestedHierarchy, and withLevelWeight; the old names raise a
MeterException naming the new one.  load() raises a MeterException and
item assignment a TypeError that name the replacement.
getMeterTerminal and makeSequence keep the 1024 and 4096 most recently used
objects with lru_cache, and TimeSignature.load keeps the defaults for the
512 most recently used meters.  The music21/meter tests make 440 terminals,
2950 sequences, and 94 meters; 161 corpus scores make 116, 376, and 27.
FrozenObject.__hash__ hashed every slot, recursively, on each call.  A
subclass that has a _frozenHash slot now keeps its hash there.
_getSlotsRecursive leaves _frozenHash out, so equality, pickling, and the
hash itself ignore it.  hash(TimeSignature('4/4').beatSequence) goes from
8.3 us to 0.14 us.
@float3

float3 commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

closing in favour of smaller PRs without the master merge (that's what made this one 10k lines): #2200 slow paths, #2201 clearer errors for old in-place calls, #2202 cache bounds, #2203 docs, #2204 optional rename (on top of #2201)

@float3 float3 closed this Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants