Source code for music21.musicxml.helpers

# ------------------------------------------------------------------------------
# Name:         musicxml/helpers.py
# Purpose:      Helper routines for musicxml export
#
# Authors:      Michael Scott Asato Cuthbert
#               Jacob Tyler Walls
#
# Copyright:    Copyright © 2013-2020 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
from __future__ import annotations

import copy
import typing as t
from xml.etree.ElementTree import tostring as et_tostring

from music21 import common
from music21 import meter
from music21.musicxml import xmlObjects
from music21 import prebase

if t.TYPE_CHECKING:
    from collections.abc import Callable
    import xml.etree.ElementTree as ET

    from music21.base import Music21Object


[docs] def dumpString(obj, *, noCopy=False) -> str: r''' wrapper around xml.etree.ElementTree that returns a string in every case and indents tags and sorts attributes. >>> from music21.musicxml.m21ToXml import Element >>> from music21.musicxml.helpers import dumpString >>> e = Element('accidental') >>> dumpString(e) '<accidental />' >>> e.text = '∆' >>> e.text == '∆' True >>> dumpString(e) '<accidental>∆</accidental>' ''' if noCopy is False: xmlEl = copy.deepcopy(obj) # adds 5% overhead else: xmlEl = obj indent(xmlEl) # adds 5% overhead for el in xmlEl.iter(): attrib = el.attrib if len(attrib) > 1: # adjust attribute order, e.g. by sorting attribs = sorted(attrib.items()) attrib.clear() attrib.update(attribs) xStr = et_tostring(xmlEl, encoding='unicode') xStr = xStr.rstrip() return xStr
[docs] def dump(obj): r''' wrapper around xml.etree.ElementTree that prints a string in every case and indents tags and sorts attributes. (Prints, does not return) >>> from music21.musicxml.helpers import dump >>> from xml.etree.ElementTree import Element >>> e = Element('accidental') >>> dump(e) <accidental /> >>> e.text = '∆' >>> e.text == '∆' True >>> dump(e) <accidental>∆</accidental> ''' print(dumpString(obj))
[docs] def indent(elem, level=0): ''' helper method, indent an element in place: ''' i = '\n' + level * ' ' lenL = len(elem) if lenL: if not elem.text or not elem.text.strip(): elem.text = i + ' ' if not elem.tail or not elem.tail.strip(): elem.tail = i subElem = None for subElem in elem: indent(subElem, level + 1) if subElem is not None: # last el subElem.tail = i if not elem.tail or not elem.tail.strip(): elem.tail = '\n' + level * ' ' else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i
[docs] def insertBeforeElements(root, insert, tagList=None): # noinspection PyShadowingNames ''' Insert element `insert` into element `root` at the earliest position of any instance of a child tag given in `tagList`. Append the element if `tagList` is `None`. >>> from xml.etree.ElementTree import fromstring as EL >>> from music21.musicxml.helpers import insertBeforeElements, dump >>> root = EL('<clef><sign>G</sign><line>4</line></clef>') >>> insert = EL('<foo/>') >>> insertBeforeElements(root, insert, tagList=['line']) >>> dump(root) <clef> <sign>G</sign> <foo /> <line>4</line> </clef> Now insert another element at the end by not specifying a tag list: >>> insert2 = EL('<bar/>') >>> insertBeforeElements(root, insert2) >>> dump(root) <clef> <sign>G</sign> <foo /> <line>4</line> <bar /> </clef> ''' if not tagList: root.append(insert) return insertIndices = {len(root)} # Iterate children only, not grandchildren for i, child in enumerate(root.findall('*')): if child.tag in tagList: insertIndices.add(i) root.insert(min(insertIndices), insert)
[docs] def measureNumberComesBefore(mNum1: str, mNum2: str) -> bool: ''' Determine whether `measureNumber1` strictly precedes `measureNumber2` given that they could involve suffixes. Equal values return False. >>> from music21.musicxml.helpers import measureNumberComesBefore >>> measureNumberComesBefore('23', '24') True >>> measureNumberComesBefore('23', '23') False >>> measureNumberComesBefore('23', '23a') True >>> measureNumberComesBefore('23a', '23b') True >>> measureNumberComesBefore('23b', '23a') False >>> measureNumberComesBefore('23b', '24a') True >>> measureNumberComesBefore('23b', '23b') False ''' def splitSuffix(measureNumber): number = '' for char in measureNumber: if char.isnumeric(): number += char else: break suffix = measureNumber[len(number):] return number, suffix if mNum1 == mNum2: return False m1Numeric, m1Suffix = splitSuffix(mNum1) m2Numeric, m2Suffix = splitSuffix(mNum2) if int(m1Numeric) != int(m2Numeric): return int(m1Numeric) < int(m2Numeric) else: sortedSuffixes = sorted([m1Suffix, m2Suffix]) return m1Suffix is sortedSuffixes[0]
[docs] def isFullMeasureRest(r: 'music21.note.Rest') -> bool: isFullMeasure = False if r.fullMeasure in (True, 'always'): isFullMeasure = True elif r.fullMeasure == 'auto': tsContext = r.getContextByClass(meter.TimeSignature) if tsContext and tsContext.barDuration.quarterLength == r.duration.quarterLength: isFullMeasure = True return isFullMeasure
[docs] def synchronizeIdsToM21(element: ET.Element, m21Object: Music21Object): ''' MusicXML 3.1 defines the id attribute (%optional-unique-id) on many elements which is perfect for setting as .id on a music21 element. <fermata id="hello"><id>bye</id></fermata> >>> from xml.etree.ElementTree import fromstring as EL >>> e = EL('<fermata id="fermata1"/>') >>> f = expressions.Fermata() >>> musicxml.helpers.synchronizeIdsToM21(e, f) >>> f.id 'fermata1' Does not change the id if the id is not specified: >>> e = EL('<fermata />') >>> f = expressions.Fermata() >>> f.id = 'doNotOverwrite' >>> musicxml.helpers.synchronizeIdsToM21(e, f) >>> f.id 'doNotOverwrite' ''' newId = element.get('id', None) if not newId: return m21Object.id = newId
[docs] def synchronizeIdsToXML( element: ET.Element, m21Object: prebase.ProtoM21Object|None ) -> None: # noinspection PyTypeChecker ''' MusicXML 3.1 defines the id attribute (entity: %optional-unique-id) on many elements which is perfect for getting from .id on a music21 element. >>> from xml.etree.ElementTree import fromstring as EL >>> e = EL('<fermata />') >>> f = expressions.Fermata() >>> f.id = 'fermata1' >>> musicxml.helpers.synchronizeIdsToXML(e, f) >>> e.get('id') 'fermata1' Does not set attr: id if el.id is not valid or default: >>> e = EL('<fermata />') >>> f = expressions.Fermata() >>> musicxml.helpers.synchronizeIdsToXML(e, f) >>> e.get('id', None) is None True >>> f.id = '123456' # invalid for MusicXML id >>> musicxml.helpers.synchronizeIdsToXML(e, f) >>> e.get('id', None) is None True None can be passed in instead of a m21object. >>> e = EL('<fermata />') >>> musicxml.helpers.synchronizeIdsToXML(e, None) >>> e.get('id', 'no idea') 'no idea' ''' # had to suppress type-checking because of spurious error on # e.get('id', 'no idea') if not isinstance(m21Object, prebase.ProtoM21Object): return if not hasattr(m21Object, 'id'): return m21Id = m21Object.id # type: ignore if m21Id is None: return if not xmlObjects.isValidXSDID(m21Id): return element.set('id', m21Id)
[docs] def setM21AttributeFromAttribute( m21El: t.Any, xmlEl: ET.Element, xmlAttributeName: str, attributeName: str|None = None, transform: Callable[[str], t.Any]|None = None, ) -> None: ''' If xmlEl has at least one element of tag==tag with some text. If it does, set the attribute either with the same name (with "foo-bar" changed to "fooBar") or with attributeName to the text contents. Pass a function or lambda function as transform to transform the value before setting it >>> from xml.etree.ElementTree import fromstring as EL >>> e = EL('<page-layout new-page="yes" page-number="4" />') >>> setb = musicxml.helpers.setM21AttributeFromAttribute >>> pl = layout.PageLayout() >>> setb(pl, e, 'page-number') >>> pl.pageNumber '4' >>> setb(pl, e, 'new-page', 'isNew') >>> pl.isNew 'yes' Transform the pageNumber value to an int. >>> setb(pl, e, 'page-number', transform=int) >>> pl.pageNumber 4 More complex: >>> convBool = musicxml.xmlObjects.yesNoToBoolean >>> setb(pl, e, 'new-page', 'isNew', transform=convBool) >>> pl.isNew True ''' value = xmlEl.get(xmlAttributeName) # find first if value is None: return if transform is not None: value = transform(value) if attributeName is None: attributeName = common.hyphenToCamelCase(xmlAttributeName) setattr(m21El, attributeName, value)
[docs] def setXMLAttributeFromAttribute( m21El: t.Any, xmlEl: ET.Element, xmlAttributeName: str, attributeName: str|None = None, transform: Callable[[t.Any], t.Any]|None = None ): ''' If m21El has at least one element of tag==tag with some text. If it does, set the attribute either with the same name (with "foo-bar" changed to "fooBar") or with attributeName to the text contents. Pass a function or lambda function as transform to transform the value before setting it >>> from xml.etree.ElementTree import fromstring as EL >>> e = EL('<page-layout/>') >>> setb = musicxml.helpers.setXMLAttributeFromAttribute >>> pl = layout.PageLayout() >>> pl.pageNumber = 4 >>> pl.isNew = True >>> setb(pl, e, 'page-number') >>> e.get('page-number') '4' >>> XB = musicxml.m21ToXml.XMLExporterBase() >>> XB.dump(e) <page-layout page-number="4" /> >>> setb(pl, e, 'new-page', 'isNew') >>> e.get('new-page') 'True' Transform the isNew value to 'yes'. >>> convBool = musicxml.xmlObjects.booleanToYesNo >>> setb(pl, e, 'new-page', 'isNew', transform=convBool) >>> e.get('new-page') 'yes' ''' if attributeName is None: attributeName = common.hyphenToCamelCase(xmlAttributeName) value = getattr(m21El, attributeName, None) if value is None: return if transform is not None: value = transform(value) xmlEl.set(xmlAttributeName, str(value))
if __name__ == '__main__': import music21 music21.mainTest() # doc tests only