Skip to content
14 changes: 11 additions & 3 deletions .agents/skills/running-tests/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,25 @@ pytest is for a module. For the whole suite use the runners, and note that a
green pytest run does not mean CI will be green -- they gather and order modules
differently.

Use `multiprocessTest` -- it runs on n-1 cores and finishes in about 10 seconds,
against roughly 45 for the single-core runner:

```bash
# everyday full run, on n-1 cores
uv run python music21/test/multiprocessTest.py
```

# exactly what GitHub Actions runs (~1 minute); use before pushing to a PR
That gap is the whole story when someone is waiting on the answer. Reach for
`testSingleCoreAll` only for what `multiprocessTest` cannot tell you: it is what
GitHub Actions runs, so it is the one to check before a release, when chasing a
CI failure that will not reproduce, or on a single-core machine.

```bash
uv run python -c 'from music21.test.testSingleCoreAll import ciMain as ci; ci()'
```

The two runners see slightly different sets of modules -- `multiprocessTest`
walks the package tree for modules reachable from `import music21`, while
`testSingleCoreAll` gathers module files from disk. Run both before a release.
`testSingleCoreAll` gathers module files from disk.

Module **order** differs between the two runners and again from pytest.
`multiprocessTest` goes in reverse-alphabetical order (with the known-slow
Expand Down
18 changes: 9 additions & 9 deletions music21/abcFormat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2505,13 +2505,15 @@ def tokenize(self, strSrc: str) -> None:
# v is up bow; might be: "^Segno"v which also should be dropped
# H is fermata
# . dot may be staccato, but should be attached to pitch
if self.currentCollectStr in ('w', 'u', 'v', 'v.', 'h', 'H', 'vk',
'uk', 'U', '~',
'.', '=', 'V', 'v.', 'S', 's',
'i', 'I', 'ui', 'u.', 'Q', 'Hy', 'Hx',
'r', 'm', 'M', 'n', 'N', 'o', 'O', 'P',
'l', 'L', 'R',
'y', 'T', 't', 'x', 'Z'):
if self.currentCollectStr in (
'w', 'u', 'v', 'v.', 'h', 'H', 'vk',
'uk', 'U', '~',
'.', '=', 'V', 'v.', 'S', 's',
'i', 'I', 'ui', 'u.', 'Q', 'Hy', 'Hx',
'r', 'm', 'M', 'n', 'N', 'o', 'O', 'P',
'l', 'L', 'R',
'y', 'T', 't', 'x', 'Z',
):
pass
# these are bad chords, or other problematic notations like
# "D.C."x
Expand Down Expand Up @@ -2785,7 +2787,6 @@ def __add__(self, other: ABCHandler) -> ABCHandler:
True
>>> ah3.tokens[3] == ah2.tokens[0]
True

'''
ah = self.__class__() # will get the same class type
ah.tokens = self.tokens + other.tokens
Expand Down Expand Up @@ -3114,7 +3115,6 @@ def _buildMeasureBoundaryIndices(
[[0, 9], [10, 16], [16, 23], [23, 29], [29, 36], [36, 42], [42, 49], [49, 56], [56, 61],
[62, 64], [64, 70], [70, 77], [77, 84], [84, 90], [90, 96],
[96, 103], [103, 110], [110, 115]]

'''
# collect start and end pairs of split
pairs = []
Expand Down
10 changes: 5 additions & 5 deletions music21/abcFormat/testFiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -633,11 +633,11 @@

# ------------------------------------------------------------------------------
ALL = [fyrareprisarn, mysteryReel, fullRiggedShip, aleIsDear, kitchGirl,
williamAndNancy, morrisonsJig, hectorTheHero, kingOfTheFairies,
sicutRosa, theAleWifesDaughter, theBeggerBoy, theBattleOfTheSnaBas,
draughtOfAle,
valentineJigg,
testPrimitive, testPrimitivePolyphonic, testPrimitiveTuplet
williamAndNancy, morrisonsJig, hectorTheHero, kingOfTheFairies,
sicutRosa, theAleWifesDaughter, theBeggerBoy, theBattleOfTheSnaBas,
draughtOfAle,
valentineJigg,
testPrimitive, testPrimitivePolyphonic, testPrimitiveTuplet
]


Expand Down
4 changes: 2 additions & 2 deletions music21/abcFormat/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1044,9 +1044,9 @@ def testNoChord(self):
score = harmony.realizeChordSymbolDurations(score)

self.assertEqual(8, score.getElementsByClass(harmony.ChordSymbol)
.last().quarterLength)
.last().quarterLength)
self.assertEqual(4, score.getElementsByClass(harmony.ChordSymbol)
.first().quarterLength)
.first().quarterLength)

def testAbcKeyImport(self):
from music21 import abcFormat
Expand Down
6 changes: 0 additions & 6 deletions music21/alpha/analysis/aligner.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,6 @@ def makeHashedStreams(self):
>>> sa2.hashedSourceStream
[NoteHashWithReference(Pitch=69, Duration=1.0, Offset=0.0),
NoteHashWithReference(Pitch=67, Duration=1.0, Offset=1.0)]

'''
if not self.preHashed:
self.hashedTargetStream = self.hasher.hashStream(self.targetStream)
Expand Down Expand Up @@ -230,7 +229,6 @@ def setupDistanceMatrix(self):
Traceback (most recent call last):
music21.alpha.analysis.aligner.AlignerException:
Cannot perform alignment with empty source stream.

'''
if not self.hashedTargetStream:
self.makeHashedStreams()
Expand Down Expand Up @@ -306,7 +304,6 @@ def populateDistanceMatrix(self):
[2, 2, 2, 4],
[4, 4, 3, 3],
[6, 6, 5, 3]])

'''

# calculate insert and delete costs based on the first tuple in the Source S
Expand Down Expand Up @@ -381,7 +378,6 @@ def getPossibleMovesFromLocation(self, i, j):

>>> sa.getPossibleMovesFromLocation(3, 0)
[0, None, None]

'''
verticalCost = int(self.distanceMatrix[i - 1][j]) if i >= 1 else None
horizontalCost = int(self.distanceMatrix[i][j - 1]) if j >= 1 else None
Expand Down Expand Up @@ -736,7 +732,6 @@ def tupleEqualityWithoutReference(self, tup1, tup2):

>>> sa.tupleEqualityWithoutReference(nhwr1, nhwr3)
False

'''
for val in tup1.hashItemsKeys:
if getattr(tup1, val) != getattr(tup2, val):
Expand Down Expand Up @@ -828,7 +823,6 @@ def calculateChangesList(self):
1
>>> saD.similarityScore
0.5

'''
i = self.n
j = self.m
Expand Down
7 changes: 3 additions & 4 deletions music21/alpha/analysis/hasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@ def _hashMIDIPitchName(self, e, thisChord=None):
>>> r = note.Rest()
>>> h._hashMIDIPitchName(r, thisChord=c)
0

'''
if thisChord and self.hashChordsAsChords:
return 1
Expand Down Expand Up @@ -417,16 +416,16 @@ def hashStream(self, s):
if self.hashChordsAsNotes:
for n in elt:
singleNoteHash = [self.hashingFunctions[hashProperty](n, thisChord=elt)
for hashProperty in self.tupleList]
for hashProperty in self.tupleList]

self.addHashToFinalHash(singleNoteHash, finalHash, n)
elif self.hashChordsAsChords:
singleNoteHash = [self.hashingFunctions[hashProperty](None, thisChord=elt)
for hashProperty in self.tupleList]
for hashProperty in self.tupleList]
self.addHashToFinalHash(singleNoteHash, finalHash, elt)
else:
singleNoteHash = [self.hashingFunctions[hashProperty](elt)
for hashProperty in self.tupleList]
for hashProperty in self.tupleList]
self.addHashToFinalHash(singleNoteHash, finalHash, elt)
# TODO: don't finalHash back and forth, return it in the smaller functions
return finalHash
Expand Down
2 changes: 1 addition & 1 deletion music21/alpha/analysis/ornamentRecognizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ def testRecognizeTrill(self):
t3Notes = stream.Stream() # C B C B C D E F
t3Notes.append(
[t3n1, t3n2, deepcopy(t3n1), deepcopy(t3n2), deepcopy(t3n1),
nachschlagN1, nachschlagN2, nachschlagN3]
nachschlagN1, nachschlagN2, nachschlagN3]
)

testConditions.append(
Expand Down
2 changes: 0 additions & 2 deletions music21/analysis/correlate.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ class ActivityMatch:

.. image:: images/ScatterWeightedPitchSpaceDynamicSymbol.*
:width: 600

'''
def __init__(self, streamObj):
if not hasattr(streamObj, 'classes') or 'Stream' not in streamObj.classes:
Expand All @@ -59,7 +58,6 @@ def _findActive(self, objNameSrc=None, objNameDst=None):
Do the analysis, finding correlations of src with dst
returns an ordered list of dictionaries, in the form
{'src': obj, 'dst': [objs]}

'''
if objNameSrc is None:
objNameSrc = (note.Note, chord.Chord)
Expand Down
6 changes: 0 additions & 6 deletions music21/analysis/discrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,14 +565,12 @@ def _likelyKeys(self, sStream):

def _bestKeyEnharmonic(self, pitchObj, mode, sStream=None):
'''

>>> ks = analysis.discrete.KrumhanslSchmuckler()
>>> s = converter.parse('tinynotation: 4/4 b-4 e- f g-')
>>> ks._bestKeyEnharmonic(pitch.Pitch('e#'), 'minor', s)
<music21.pitch.Pitch F>
>>> ks._bestKeyEnharmonic(pitch.Pitch('f-'), 'major', s)
<music21.pitch.Pitch E>

'''
if pitchObj is None:
return None
Expand Down Expand Up @@ -881,7 +879,6 @@ def getWeights(self, weightType='major'):
12
>>> a.getWeights('major')
[16.8..., 0.8..., 12.9..., 1.4..., ...]

'''
weightType = weightType.lower()
# note: only one value is different from KrumhanslSchmuckler
Expand Down Expand Up @@ -1045,7 +1042,6 @@ def getPitchSpan(self, subStream) -> tuple[pitch.Pitch, pitch.Pitch]|None:
>>> s.insert(4, harmony.ChordSymbol('C6'))
>>> p.getPitchSpan(s) is None
True

'''
if subStream is self._referenceStream and self.minPitchObj and self.maxPitchObj:
return self.minPitchObj, self.maxPitchObj
Expand Down Expand Up @@ -1118,7 +1114,6 @@ def solutionLegend(self, compress: bool = False) -> list[
>>> x = p.process(s.parts[1])
>>> [len(y) for y in [x for x in p.solutionLegend(compress=True)]]
[2, 2]

'''
colorsUsed = []
if compress:
Expand Down Expand Up @@ -1160,7 +1155,6 @@ def solutionUnitString(self):

def solutionToColor(self, solution: int|None) -> str:
'''

>>> p = analysis.discrete.Ambitus()
>>> s = stream.Stream()
>>> c = chord.Chord(['a2', 'b4', 'c8'])
Expand Down
1 change: 0 additions & 1 deletion music21/analysis/harmonicFunction.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,6 @@ def functionToRoman(thisHarmonicFunction: HarmonicFunction,
>>> rn = roman.RomanNumeral('vi')
>>> str(analysis.harmonicFunction.romanToFunction(rn))
'Tp'

'''
if isinstance(keyOrScale, str):
keyOrScale = key.Key(keyOrScale)
Expand Down
1 change: 0 additions & 1 deletion music21/analysis/metrical.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ def thomassenMelodicAccent(streamIn: stream.Stream):
('E4', 0.5561)
('D4', 0.17)
('D4', 0.0)

'''
# we use .ps instead of Intervals for speed, since
# we just need perceived contours
Expand Down
2 changes: 1 addition & 1 deletion music21/analysis/patel.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def melodicIntervalVariability(streamForAnalysis, **skipKeywords):
totalElements = len(intervalStream)
if totalElements < 2: # this is correct.
raise ValueError('need at least three notes to have '
+ 'a std-deviation of intervals (and thus a MIV)')
+ 'a std-deviation of intervals (and thus a MIV)')
# summation = 0
semitoneList = [myInt.chromatic.undirected for myInt in intervalStream]
return 100 * (stdev(semitoneList) / mean(semitoneList))
Expand Down
1 change: 0 additions & 1 deletion music21/analysis/reduceChordsOld.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ def reduceMeasureToNChords(self,

def computeMeasureChordWeights(self, measureObj, weightAlgorithm=None):
'''

>>> s = analysis.reduceChordsOld.testMeasureStream1().notes
>>> cr = analysis.reduceChordsOld.ChordReducer()
>>> cws = cr.computeMeasureChordWeights(s)
Expand Down
20 changes: 12 additions & 8 deletions music21/analysis/reduction.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,6 @@ class PartReduction:
of all parts. The default is True.

If the `normalize` parameter is False, no normalization will take place. The default is True.

'''
def __init__(self,
srcScore=None,
Expand Down Expand Up @@ -734,9 +733,14 @@ def _dynamicToWeight(targets):
offsetEnd = offsetStart + ds['span']
# get all targets within the contiguous region
# e.g., Dynamics objects
match = flatRef.getElementsByOffset(offsetStart, offsetEnd,
includeEndBoundary=True, mustFinishInSpan=False,
mustBeginInSpan=True).getElementsByClass(target).stream()
inRegion = flatRef.getElementsByOffset(
offsetStart,
offsetEnd,
includeEndBoundary=True,
mustFinishInSpan=False,
mustBeginInSpan=True,
)
match = inRegion.getElementsByClass(target).stream()
# environLocal.printDebug(['matched elements', target, match])
# extend duration of all found dynamics
match.extendDuration(target, inPlace=True)
Expand Down Expand Up @@ -1252,7 +1256,7 @@ def testPartReductionE(self):
s.insert(0, p2)
# s.show()
pr = analysis.reduction.PartReduction(s, fillByMeasure=True,
segmentByTarget=False, normalize=False)
segmentByTarget=False, normalize=False)
pr.process()
target = pr.getGraphHorizontalBarWeightedData()
match = [(0, [[0.0, 4.0, 0.178571428571, '#666666'],
Expand All @@ -1265,7 +1269,7 @@ def testPartReductionE(self):
self._matchWeightedData(match, target)

pr = analysis.reduction.PartReduction(s, fillByMeasure=False,
segmentByTarget=True, normalize=False)
segmentByTarget=True, normalize=False)
pr.process()
target = pr.getGraphHorizontalBarWeightedData()
match = [(0, [[0.0, 2.0, 0.05, '#666666'],
Expand All @@ -1282,7 +1286,7 @@ def testPartReductionE(self):


pr = analysis.reduction.PartReduction(s, fillByMeasure=False,
segmentByTarget=False)
segmentByTarget=False)
pr.process()
target = pr.getGraphHorizontalBarWeightedData()
# print(target)
Expand All @@ -1296,7 +1300,7 @@ def testPartReductionE(self):


pr = analysis.reduction.PartReduction(s, fillByMeasure=True,
segmentByTarget=True)
segmentByTarget=True)
pr.process()
target = pr.getGraphHorizontalBarWeightedData()
match = [(0, [[0.0, 2.0, 0.3888888888888, '#666666'],
Expand Down
2 changes: 0 additions & 2 deletions music21/analysis/windowed.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ def getMinimumWindowStream(self, timeSignature='1/4'):

>>> wa1 = analysis.windowed.WindowedAnalysis(s, p)
>>> wa2 = analysis.windowed.WindowedAnalysis(s.flatten(), p)

'''
# create a stream that contains just a 1/4 time signature; this is
# the minimum window size (and partitioning will be done by measure)
Expand Down Expand Up @@ -151,7 +150,6 @@ def analyze(self, windowSize, windowType='overlap'):
>>> a, b = wa.analyze(1, windowType='adjacentAverage')
>>> len(a), len(b)
(36, 36)

'''
maxWindowCount = len(self._windowedStream)
# assuming that this is sorted
Expand Down
2 changes: 0 additions & 2 deletions music21/articulations.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ class TimbreArticulation(Articulation):
# ------------------------------------------------------------------------------
class Accent(DynamicArticulation):
'''

>>> a = articulations.Accent()
'''
def __init__(self, **keywords):
Expand All @@ -246,7 +245,6 @@ def __init__(self, **keywords):

class Staccato(LengthArticulation):
'''

>>> a = articulations.Staccato()
'''
def __init__(self, **keywords):
Expand Down
3 changes: 0 additions & 3 deletions music21/audioSearch/scoreFollower.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ def repeatTranscription(self):
False
>>> print(ScF.lastNotePosition)
10

'''
from music21 import audioSearch

Expand Down Expand Up @@ -382,7 +381,6 @@ def updatePosition(self, prob, totalLengthPeriod, time_start):
>>> exitType = ScF.updatePosition(prob, totalLengthPeriod, time_start)
>>> print(exitType)
countdownExceeded

'''
exitType = False

Expand Down Expand Up @@ -462,7 +460,6 @@ def predictNextNotePosition(self, totalLengthPeriod, totalSeconds):
... totalLengthPeriod, totalSeconds)
>>> print(predictedStartPosition)
18

'''
extraLength = totalLengthPeriod * totalSeconds / self.seconds_recording
middleRhythm = 0
Expand Down
Loading
Loading