Data Labels
ci / checks (push) Failing after 3h12m18s

This commit is contained in:
khodowany
2026-09-20 02:07:26 -05:00
parent 5fc2103810
commit 7bb1ed2b13
24 changed files with 2334 additions and 2563 deletions
+16 -55
View File
@@ -38,7 +38,6 @@ class Importer:
self.staff_details = {'lines': 5, 'tunings': []}
self.absolute = Fraction()
self.tremolo_starts = {}
self.has_repeats = False
def fail(self, text: str):
raise MusicXMLError(f'{self.location}: {text}')
def children(self, node, allowed: set[str]) -> None:
@@ -178,8 +177,6 @@ class Importer:
self.harmony_sequence += 1
result = {'id': f'harmony-{self.harmony_sequence}', 'onset': rational(self.offset(node, cursor)), 'kind': kind.text, 'degrees': []}
self.attrs(kind, {'text', 'use-symbols', 'stack-degrees', 'parentheses-degrees', 'bracket-degrees'})
if 'text' in kind.attrib:
result['kind_text'] = kind.get('text')
for tag in ('root', 'bass'):
item = node.find(tag)
if item is None:
@@ -204,17 +201,9 @@ class Importer:
elif operation == 'subtract' and degree_alter:
self.fail('cannot subtract a degree different from the one in the chord kind')
result['degrees'].append({'value': value, 'alter': rational(degree_alter), 'type': operation})
if kind.get('text') and result['kind'] != 'none':
harmony.normalize_text(result, kind.get('text'))
chord_degrees = harmony.degrees(result)
if 'kind_text' in result and result['kind'] != 'none':
label = 'C' + result['kind_text']
try:
displayed = harmony.degrees(harmony.parse_symbol(label))
except ValueError:
self.fail(f'unmapped harmony display text {label!r}; use a standard chord kind/degree spelling')
if displayed != chord_degrees:
if displayed != harmony.KINDS[result['kind']]:
self.fail(f'contradictory harmony text {label!r} and structured chord degrees')
result.pop('kind_text')
if node.find('inversion') is not None:
inversion = self.number(node.findtext('inversion'), integer=True)
result['inversion'] = inversion
@@ -235,6 +224,8 @@ class Importer:
self.children(node, {'direction-type', 'offset', 'staff', 'voice', 'sound'})
onset = self.offset(node, cursor)
for group in node.findall('direction-type'):
if group.find('segno') is not None or group.find('coda') is not None:
self.fail('navigation jumps are unsupported; write the music out in reading order')
self.children(group, {'words', 'octave-shift', 'rehearsal', 'metronome'})
for item in group:
if item.tag == 'words':
@@ -275,6 +266,8 @@ class Importer:
if sound is not None:
self.sound(sound, onset, measure)
def sound(self, node, onset: Fraction, measure: dict) -> None:
if {'dacapo', 'dalsegno', 'segno', 'coda', 'tocoda', 'fine', 'forward-repeat', 'time-only'} & node.attrib.keys():
self.fail('navigation jumps are unsupported; write the music out in reading order')
self.attrs(node, {'tempo'})
self.children(node, set())
if node.get('tempo') and not any(fraction(t['onset']) == onset for t in measure.get('tempos', [])):
@@ -375,8 +368,8 @@ class Importer:
ties = {t.get('type') for t in node.findall('tie')}
for tie in node.findall('tie'):
self.attrs(tie, {'type', 'time-only'})
if tie.get('time-only') and not self.has_repeats:
self.fail('time-only tie requires a repeated section')
if 'time-only' in tie.attrib:
self.fail('pass-specific ties are unsupported; write the music out in reading order')
notations = node.find('notations')
visual_ties = set()
if notations is not None:
@@ -387,22 +380,12 @@ class Importer:
ties |= visual_ties
if ties - {'start', 'stop'}:
self.fail('unsupported tie notation')
if self.has_repeats and ties:
from generator.repeats import pass_numbers
p['sound_ties'] = []
p['printed_ties'] = sorted(visual_ties)
for tie in node.findall('tie'):
self.attrs(tie, {'type', 'time-only'})
marker = {'type': tie.get('type')}
if tie.get('time-only'):
marker['passes'] = pass_numbers(tie.get('time-only'))
p['sound_ties'].append(marker)
if not self.has_repeats and 'stop' in ties:
if 'stop' in ties:
source = self.open_ties.pop(value, None)
if source is None or source['_absolute'] + fraction(source['duration']) != p['_absolute']:
self.fail('tie stop has no contiguous matching start')
source['tie_to'], p['tie_from'] = p['id'], source['id']
if not self.has_repeats and 'start' in ties:
if 'start' in ties:
if value in self.open_ties:
self.fail('ambiguous duplicate tie at the same pitch')
self.open_ties[value] = p
@@ -512,7 +495,6 @@ class Importer:
root = ElementTree.parse(self.path, forbid_entities=True, forbid_external=True).getroot()
if root.tag != 'score-partwise' or root.get('version') not in ('3.1', '4.0'):
self.fail('expected uncompressed score-partwise MusicXML 3.1 or 4.0')
self.has_repeats = bool(root.findall('.//barline/repeat') or root.findall('.//barline/ending'))
self.children(root, IGNORED_TOP | {'part'})
parts = root.findall('part')
definitions = root.findall('part-list/score-part')
@@ -564,8 +546,6 @@ class Importer:
if item.tag == 'attributes':
state = self.attributes(item, cursor)
if cursor == 0:
if self.has_repeats:
measure.setdefault('initial_attributes', {}).update({k: v for k, v in state.items() if k != 'onset'})
for k in ('key', 'meter', 'clef', 'transpose', 'staff_details'):
measure[k] = copy.deepcopy(getattr(self, k))
elif len(state) > 1:
@@ -599,11 +579,13 @@ class Importer:
extent = max(extent, cursor)
previous = None
elif item.tag == 'barline':
if item.find('repeat') is not None or item.find('ending') is not None:
self.fail('repeats and numbered endings are unsupported; write the music out in reading order')
self.attrs(item, {'location'})
self.children(item, {'bar-style', 'repeat', 'ending'})
self.children(item, {'bar-style'})
side = item.get('location', 'right')
if side not in ('left', 'right'):
self.fail('barlines and repeats must be at measure boundaries')
self.fail('barlines must be at measure boundaries')
if side in measure.get('barlines', {}):
self.fail(f'duplicate {side} barline')
if side == 'left' and cursor != 0 or side == 'right' and cursor != extent:
@@ -614,26 +596,6 @@ class Importer:
measure.setdefault('barlines', {})[side] = style
if side == 'right':
measure['_right_barline_onset'] = cursor
for repeat in item.findall('repeat'):
self.attrs(repeat, {'direction', 'times', 'winged'})
self.children(repeat, set())
if side in measure.get('repeats', {}):
self.fail('duplicate repeat marker')
marker = {'direction': repeat.get('direction')}
if repeat.get('times'):
if marker['direction'] != 'backward':
self.fail('repeat times requires a backward repeat')
marker['times'] = self.number(repeat.get('times'), integer=True, positive=True)
if repeat.get('winged', 'none') != 'none':
self.fail('winged repeat engraving is not supported')
measure.setdefault('repeats', {})[side] = marker
for ending in item.findall('ending'):
from generator.repeats import pass_numbers
self.attrs(ending, {'number', 'type', 'print-object', 'end-length', 'text-x', 'text-y'})
self.children(ending, set())
if side in measure.get('endings', {}):
self.fail('duplicate ending marker')
measure.setdefault('endings', {})[side] = {'type': ending.get('type'), 'numbers': pass_numbers(ending.get('number', '')), 'text': ending.text or '', 'visible': ending.get('print-object', 'yes') != 'no'}
if not voices or extent <= 0:
self.fail('empty or zero-duration measure')
cursor = extent
@@ -653,6 +615,8 @@ class Importer:
if len(matches) != 1 or onset in used:
self.fail('optional-chord marker must identify exactly one harmony and occur only once')
matches[0]['chord_optional'] = True
matches[0]['parenthesized'] = True
matches[0]['symbol'] = harmony.symbol(matches[0])
used.add(onset)
measures.append(measure)
self.absolute += cursor
@@ -667,9 +631,6 @@ class Importer:
metadata.update({'key_signature': measures[0]['key_signature'], 'time_signature': measures[0]['time_signature'], 'key': copy.deepcopy(measures[0]['key'])})
result = {'schema_version': SCHEMA_VERSION, 'metadata': metadata, 'staves': [{'id': 'staff-source', 'name': definitions[0].findtext('part-name', 'Source'), 'role': 'vocal', 'clef': copy.deepcopy(measures[0]['clef']), 'tuplets': self.tuplets, 'octave_shifts': self.octaves, 'measures': measures}]}
result['staves'][0]['instruments'] = instruments
if self.has_repeats:
from generator.repeats import expand_document
validate_document(expand_document(result))
validate_document(result)
return result