X-Git-Url: https://git.donarmstrong.com/?a=blobdiff_plain;f=python%2Fmusicxml.py;h=431bcd695d69949e500d533c1fb2b23d5e4ebb50;hb=81abebcfecdb22d2e7b6b8e2a6d9f758aa45a13a;hp=f028cce8a3a50c8d531040f6b0683f97e26eb030;hpb=daf9c2c65d29cb29d1e895bc5e4f4f5f63a40924;p=lilypond.git diff --git a/python/musicxml.py b/python/musicxml.py index f028cce8a3..431bcd695d 100644 --- a/python/musicxml.py +++ b/python/musicxml.py @@ -5,9 +5,9 @@ from rational import * import re import sys import copy -import lilylib +import lilylib as ly -_ = lilylib._ +_ = ly._ def error (str): ly.stderr_write ((_ ("error: %s") % str) + "\n") @@ -15,12 +15,35 @@ def error (str): def escape_ly_output_string (input_string): return_string = input_string - needs_quotes = not re.match (u"^[a-zA-ZäöüÜÄÖßñ]*$", return_string); + needs_quotes = not re.match (u"^[a-zA-ZäöüÜÄÖß,\.!:ñ]*$", return_string); if needs_quotes: return_string = "\"" + string.replace (return_string, "\"", "\\\"") + "\"" return return_string +def musicxml_duration_to_log (dur): + return {'256th': 8, + '128th': 7, + '64th': 6, + '32nd': 5, + '16th': 4, + 'eighth': 3, + 'quarter': 2, + 'half': 1, + 'whole': 0, + 'breve': -1, + 'longa': -2, + 'long': -2}.get (dur, 0) + +def interpret_alter_element (alter_elm): + alter = 0 + if alter_elm: + val = eval(alter_elm.get_text ()) + if type (val) in (int, float): + alter = val + return alter + + class Xml_node: def __init__ (self): self._children = [] @@ -51,13 +74,25 @@ class Xml_node: return ''.join ([c.get_text () for c in self._children]) def message (self, msg): - lilylib.stderr_write (msg+'\n') + ly.stderr_write (msg+'\n') p = self while p: - sys.stderr.write (' In: <%s %s>\n' % (p._name, ' '.join (['%s=%s' % item for item in p._attribute_dict.items()]))) + sys.stderr.write (' In: <%s %s>\n' % (p._name, ' '.join (['%s=%s' % item for item in p._attribute_dict.items ()]))) p = p.get_parent () + def dump (self, indent = ''): + sys.stderr.write ('%s<%s%s>' % (indent, self._name, ''.join ([' %s=%s' % item for item in self._attribute_dict.items ()]))) + non_text_children = [c for c in self._children if not isinstance (c, Hash_text)] + if non_text_children: + sys.stderr.write ('\n') + for c in self._children: + c.dump (indent + " ") + if non_text_children: + sys.stderr.write (indent) + sys.stderr.write ('\n' % self._name) + + def get_typed_children (self, klass): if not klass: return [] @@ -127,11 +162,11 @@ class Work (Xml_node): class Identification (Xml_node): def get_rights (self): - rights = self.get_maybe_exist_named_child ('rights') - if rights: - return rights.get_text () - else: - return '' + rights = self.get_named_children ('rights') + ret = [] + for r in rights: + ret.append (r.get_text ()) + return string.join (ret, "\n") def get_creator (self, type): creators = self.get_named_children ('creator') @@ -189,6 +224,15 @@ class Identification (Xml_node): software.append (s.get_text ()) return software + def get_file_description (self): + misc = self.get_named_children ('miscellaneous') + for m in misc: + misc_fields = m.get_named_children ('miscellaneous-field') + for mf in misc_fields: + if hasattr (mf, 'name') and mf.name == 'description': + return mf.get_text () + return None + class Duration (Music_xml_node): @@ -199,7 +243,8 @@ class Duration (Music_xml_node): class Hash_comment (Music_xml_node): pass class Hash_text (Music_xml_node): - pass + def dump (self, indent = ''): + sys.stderr.write ('%s' % string.strip (self._data)) class Pitch (Music_xml_node): def get_step (self): @@ -214,10 +259,7 @@ class Pitch (Music_xml_node): def get_alteration (self): ch = self.get_maybe_exist_typed_child (get_class (u'alter')) - alter = 0 - if ch: - alter = int (ch.get_text ().strip ()) - return alter + return interpret_alter_element (ch) class Unpitched (Music_xml_node): def get_step (self): @@ -243,15 +285,28 @@ class Measure_element (Music_xml_node): return None def is_first (self): - cn = self._parent.get_typed_children (self.__class__) - cn = [c for c in cn if c.get_voice_id () == self.get_voice_id ()] + # Look at all measure elements (previously we had self.__class__, which + # only looked at objects of the same type! + cn = self._parent.get_typed_children (Measure_element) + # But only look at the correct voice; But include Attributes, too, which + # are not tied to any particular voice + cn = [c for c in cn if (c.get_voice_id () == self.get_voice_id ()) or isinstance (c, Attributes)] return cn[0] == self class Attributes (Measure_element): def __init__ (self): Measure_element.__init__ (self) self._dict = {} + self._original_tag = None + self._time_signature_cache = None + def is_first (self): + cn = self._parent.get_typed_children (self.__class__) + if self._original_tag: + return cn[0] == self._original_tag + else: + return cn[0] == self + def set_attributes_from_previous (self, dict): self._dict.update (dict) @@ -261,25 +316,63 @@ class Attributes (Measure_element): def get_named_attribute (self, name): return self._dict.get (name) + + def single_time_sig_to_fraction (self, sig): + if len (sig) < 2: + return 0 + n = 0 + for i in sig[0:-1]: + n += i + return Rational (n, sig[-1]) def get_measure_length (self): - (n,d) = self.get_time_signature () - return Rational (n,d) + sig = self.get_time_signature () + if not sig or len (sig) == 0: + return 1 + if isinstance (sig[0], list): + # Complex compound time signature + l = 0 + for i in sig: + l += self.single_time_sig_to_fraction (i) + return l + else: + # Simple (maybe compound) time signature of the form (beat, ..., type) + return self.single_time_sig_to_fraction (sig) + return 0 def get_time_signature (self): - "return time sig as a (beat, beat-type) tuple" + "Return time sig as a (beat, beat-type) tuple. For compound signatures," + "return either (beat, beat,..., beat-type) or ((beat,..., type), " + "(beat,..., type), ...)." + if self._time_signature_cache: + return self._time_signature_cache try: mxl = self.get_named_attribute ('time') - if mxl: - beats = mxl.get_maybe_exist_named_child ('beats') - type = mxl.get_maybe_exist_named_child ('beat-type') - return (int (beats.get_text ()), - int (type.get_text ())) - else: + if not mxl: + return None + + if mxl.get_maybe_exist_named_child ('senza-misura'): + # TODO: Handle pieces without a time signature! + error (_ ("Senza-misura time signatures are not yet supported!")) return (4, 4) - except KeyError: - error (_ ("requested time signature, but time sig is unknown")) + else: + signature = [] + current_sig = [] + for i in mxl.get_all_children (): + if isinstance (i, Beats): + beats = string.split (i.get_text ().strip (), "+") + current_sig = [int (j) for j in beats] + elif isinstance (i, BeatType): + current_sig.append (int (i.get_text ())) + signature.append (current_sig) + current_sig = [] + if isinstance (signature[0], list) and len (signature) == 1: + signature = signature[0] + self._time_signature_cache = signature + return signature + except (KeyError, ValueError): + self.message (_ ("Unable to interpret time signature! Falling back to 4/4.")) return (4, 4) # returns clef information in the form ("cleftype", position, octave-shift) @@ -300,16 +393,55 @@ class Attributes (Measure_element): return clefinfo def get_key_signature (self): - "return (fifths, mode) tuple" + "return (fifths, mode) tuple if the key signatures is given as " + "major/minor in the Circle of fifths. Otherwise return an alterations" + "list of the form [[step,alter<,octave>], [step,alter<,octave>], ...], " + "where the octave values are optional." key = self.get_named_attribute ('key') - mode_node = key.get_maybe_exist_named_child ('mode') - mode = 'major' - if mode_node: - mode = mode_node.get_text () + if not key: + return None + fifths_elm = key.get_maybe_exist_named_child ('fifths') + if fifths_elm: + mode_node = key.get_maybe_exist_named_child ('mode') + mode = None + if mode_node: + mode = mode_node.get_text () + if not mode or mode == '': + mode = 'major' + fifths = int (fifths_elm.get_text ()) + # TODO: Shall we try to convert the key-octave and the cancel, too? + return (fifths, mode) + else: + alterations = [] + current_step = 0 + for i in key.get_all_children (): + if isinstance (i, KeyStep): + current_step = int (i.get_text ()) + elif isinstance (i, KeyAlter): + alterations.append ([current_step, interpret_alter_element (i)]) + elif isinstance (i, KeyOctave): + nr = -1 + if hasattr (i, 'number'): + nr = int (i.number) + if (nr > 0) and (nr <= len (alterations)): + # MusicXML Octave 4 is middle C -> shift to 0 + alterations[nr-1].append (int (i.get_text ())-4) + else: + i.message (_ ("Key alteration octave given for a " + "non-existing alteration nr. %s, available numbers: %s!") % (nr, len(alterations))) + return alterations + + def get_transposition (self): + return self.get_named_attribute ('transpose') + +class KeyAlter (Music_xml_node): + pass +class KeyStep (Music_xml_node): + pass +class KeyOctave (Music_xml_node): + pass - fifths = int (key.get_maybe_exist_named_child ('fifths').get_text ()) - return (fifths, mode) class Barline (Measure_element): pass @@ -324,29 +456,34 @@ class Note (Measure_element): def __init__ (self): Measure_element.__init__ (self) self.instrument_name = '' - + self._after_grace = False + def is_grace (self): + return self.get_maybe_exist_named_child (u'grace') + def is_after_grace (self): + if not self.is_grace(): + return False; + gr = self.get_maybe_exist_typed_child (Grace) + return self._after_grace or hasattr (gr, 'steal-time-previous'); + def get_duration_log (self): ch = self.get_maybe_exist_named_child (u'type') if ch: log = ch.get_text ().strip() - return {'256th': 8, - '128th': 7, - '64th': 6, - '32nd': 5, - '16th': 4, - 'eighth': 3, - 'quarter': 2, - 'half': 1, - 'whole': 0, - 'breve': -1, - 'long': -2}.get (log, 0) + return musicxml_duration_to_log (log) elif self.get_maybe_exist_named_child (u'grace'): # FIXME: is it ok to default to eight note for grace notes? return 3 else: - self.message (_ ("Encountered note at %s with %s duration (no element):") % (self.start, self.duration) ) - return 0 + return None + + def get_duration_info (self): + log = self.get_duration_log () + if log != None: + dots = len (self.get_typed_children (Dot)) + return (log, dots) + else: + return None def get_factor (self): return 1 @@ -379,7 +516,7 @@ class Part_list (Music_xml_node): if instrument_name: return instrument_name else: - lilylib.stderr_write (_ ("Unable to find find instrument for ID=%s\n") % id) + ly.stderr_write (_ ("Unable to find instrument for ID=%s\n") % id) return "Grand Piano" class Part_group (Music_xml_node): @@ -400,6 +537,10 @@ class Syllabic (Music_xml_node): def continued (self): text = self.get_text() return (text == "begin") or (text == "middle") +class Elision (Music_xml_node): + pass +class Extend (Music_xml_node): + pass class Text (Music_xml_node): pass @@ -410,32 +551,6 @@ class Lyric (Music_xml_node): else: return -1 - def lyric_to_text (self): - continued = False - syllabic = self.get_maybe_exist_typed_child (Syllabic) - if syllabic: - continued = syllabic.continued () - text = self.get_maybe_exist_typed_child (Text) - - if text: - text = text.get_text() - # We need to convert soft hyphens to -, otherwise the ascii codec as well - # as lilypond will barf on that character - text = string.replace( text, u'\xad', '-' ) - - if text == "-" and continued: - return "--" - elif text == "_" and continued: - return "__" - elif continued and text: - return escape_ly_output_string (text) + " --" - elif continued: - return "--" - elif text: - return escape_ly_output_string (text) - else: - return "" - class Musicxml_voice: def __init__ (self): self._elements = [] @@ -474,6 +589,13 @@ class Musicxml_voice: return self._lyrics +def graces_to_aftergraces (pending_graces): + for gr in pending_graces: + gr._when = gr._prev_when + gr._measure_position = gr._prev_measure_position + gr._after_grace = True + + class Part (Music_xml_node): def __init__ (self): Music_xml_node.__init__ (self) @@ -486,7 +608,7 @@ class Part (Music_xml_node): n = n._parent return n.get_named_child ('part-list') - + def interpret (self): """Set durations and starting points.""" """The starting point of the very first note is 0!""" @@ -504,6 +626,9 @@ class Part (Music_xml_node): measure_start_moment = now is_first_measure = True previous_measure = None + # Graces at the end of a measure need to have their position set to the + # previous number! + pending_graces = [] for m in measures: # implicit measures are used for artificial measures, e.g. when # a repeat bar line splits a bar into two halves. In this case, @@ -529,6 +654,14 @@ class Part (Music_xml_node): measure_position = Rational (0) for n in m.get_all_children (): + # figured bass has a duration, but applies to the next note + # and should not change the current measure position! + if isinstance (n, FiguredBass): + n._divisions = factor.denominator () + n._when = now + n._measure_position = measure_position + continue + if isinstance (n, Hash_text): continue dur = Rational (0) @@ -549,6 +682,9 @@ class Part (Music_xml_node): if n.get_name() == 'backup': dur = - dur + # reset all graces before the backup to after-graces: + graces_to_aftergraces (pending_graces) + pending_graces = [] if n.get_maybe_exist_typed_child (Grace): dur = Rational (0) @@ -559,13 +695,30 @@ class Part (Music_xml_node): rest._is_whole_measure = True - if (dur > Rational (0) + if (dur > Rational (0) and n.get_maybe_exist_typed_child (Chord)): now = last_moment measure_position = last_measure_position n._when = now n._measure_position = measure_position + + # For all grace notes, store the previous note, in case need + # to turn the grace note into an after-grace later on! + if isinstance(n, Note) and n.is_grace (): + n._prev_when = last_moment + n._prev_measure_position = last_measure_position + # After-graces are placed at the same position as the previous note + if isinstance(n, Note) and n.is_after_grace (): + # TODO: We should do the same for grace notes at the end of + # a measure with no following note!!! + n._when = last_moment + n._measure_position = last_measure_position + elif isinstance(n, Note) and n.is_grace (): + pending_graces.append (n) + elif (dur > Rational (0)): + pending_graces = []; + n._duration = dur if dur > Rational (0): last_moment = now @@ -587,6 +740,9 @@ class Part (Music_xml_node): if instrument: n.instrument_name = part_list.get_instrument (instrument.id) + # reset all graces at the end of the measure to after-graces: + graces_to_aftergraces (pending_graces) + pending_graces = [] # Incomplete first measures are not padded, but registered as partial if is_first_measure: is_first_measure = False @@ -601,12 +757,18 @@ class Part (Music_xml_node): # modify attributes so that only those applying to the given staff remain def extract_attributes_for_staff (part, attr, staff): attributes = copy.copy (attr) - attributes._children = copy.copy (attr._children) + attributes._children = []; attributes._dict = attr._dict.copy () - for c in attributes._children: - if hasattr (c, 'number') and c.number != staff: - attributes._children.remove (c) - return attributes + attributes._original_tag = attr + # copy only the relevant children over for the given staff + for c in attr._children: + if (not (hasattr (c, 'number') and (c.number != staff)) and + not (isinstance (c, Hash_text))): + attributes._children.append (c) + if not attributes._children: + return None + else: + return attributes def extract_voices (part): voices = {} @@ -624,6 +786,8 @@ class Part (Music_xml_node): vid = None if voice_id: vid = voice_id.get_text () + elif isinstance (n, Note): + vid = "None" staff_id = n.get_maybe_exist_named_child (u'staff') sid = None @@ -653,10 +817,18 @@ class Part (Music_xml_node): id = None for n in elements: voice_id = n.get_maybe_exist_typed_child (get_class ('voice')) + if voice_id: + id = voice_id.get_text () + else: + id = "None" - if not (voice_id or isinstance (n, Attributes) or + # We don't need backup/forward any more, since we have already + # assigned the correct onset times. + # TODO: Let Grouping through. Also: link, print, bokmark sound + if not (isinstance (n, Note) or isinstance (n, Attributes) or isinstance (n, Direction) or isinstance (n, Partial) or - isinstance (n, Barline) or isinstance (n, Harmony) ): + isinstance (n, Barline) or isinstance (n, Harmony) or + isinstance (n, FiguredBass) ): continue if isinstance (n, Attributes) and not start_attr: @@ -667,8 +839,9 @@ class Part (Music_xml_node): # assign these only to the voices they really belongs to! for (s, vids) in staff_to_voice_dict.items (): staff_attributes = part.extract_attributes_for_staff (n, s) - for v in vids: - voices[v].add_element (staff_attributes) + if staff_attributes: + for v in vids: + voices[v].add_element (staff_attributes) continue if isinstance (n, Partial) or isinstance (n, Barline): @@ -688,13 +861,12 @@ class Part (Music_xml_node): voices[v].add_element (n) continue - if isinstance (n, Harmony): - # store the harmony element until we encounter the next note - # and assign it only to that one voice. + if isinstance (n, Harmony) or isinstance (n, FiguredBass): + # store the harmony or figured bass element until we encounter + # the next note and assign it only to that one voice. assign_to_next_note.append (n) continue - id = voice_id.get_text () if hasattr (n, 'print-object') and getattr (n, 'print-object') == "no": #Skip this note. pass @@ -744,6 +916,16 @@ class Time_modification(Music_xml_node): a = self.get_maybe_exist_named_child ('normal-notes') return (int(a.get_text ()), int (b.get_text ())) + def get_normal_type (self): + tuplet_type = self.get_maybe_exist_named_child ('normal-type') + if tuplet_type: + dots = self.get_named_children ('normal-dot') + log = musicxml_duration_to_log (tuplet_type.get_text ().strip ()) + return (log , len (dots)) + else: + return None + + class Accidental (Music_xml_node): def __init__ (self): Music_xml_node.__init__ (self) @@ -766,7 +948,40 @@ class Wedge (Music_xml_spanner): pass class Tuplet (Music_xml_spanner): - pass + def duration_info_from_tuplet_note (self, tuplet_note): + tuplet_type = tuplet_note.get_maybe_exist_named_child ('tuplet-type') + if tuplet_type: + dots = tuplet_note.get_named_children ('tuplet-dot') + log = musicxml_duration_to_log (tuplet_type.get_text ().strip ()) + return (log, len (dots)) + else: + return None + + # Return tuplet note type as (log, dots) + def get_normal_type (self): + tuplet = self.get_maybe_exist_named_child ('tuplet-normal') + if tuplet: + return self.duration_info_from_tuplet_note (tuplet) + else: + return None + + def get_actual_type (self): + tuplet = self.get_maybe_exist_named_child ('tuplet-actual') + if tuplet: + return self.duration_info_from_tuplet_note (tuplet) + else: + return None + + def get_tuplet_note_count (self, tuplet_note): + if tuplet_note: + tuplet_nr = tuplet_note.get_maybe_exist_named_child ('tuplet-number') + if tuplet_nr: + return int (tuplet_nr.get_text ()) + return None + def get_normal_nr (self): + return self.get_tuplet_note_count (self.get_maybe_exist_named_child ('tuplet-normal')) + def get_actual_nr (self): + return self.get_tuplet_note_count (self.get_maybe_exist_named_child ('tuplet-actual')) class Bracket (Music_xml_spanner): pass @@ -848,10 +1063,7 @@ class DirType (Music_xml_node): class Bend (Music_xml_node): def bend_alter (self): alter = self.get_maybe_exist_named_child ('bend-alter') - if alter: - return alter.get_text() - else: - return 0 + return interpret_alter_element (alter) class Words (Music_xml_node): pass @@ -859,6 +1071,42 @@ class Words (Music_xml_node): class Harmony (Music_xml_node): pass +class ChordPitch (Music_xml_node): + def step_class_name (self): + return u'root-step' + def alter_class_name (self): + return u'root-alter' + def get_step (self): + ch = self.get_unique_typed_child (get_class (self.step_class_name ())) + return ch.get_text ().strip () + def get_alteration (self): + ch = self.get_maybe_exist_typed_child (get_class (self.alter_class_name ())) + return interpret_alter_element (ch) + +class Root (ChordPitch): + pass + +class Bass (ChordPitch): + def step_class_name (self): + return u'bass-step' + def alter_class_name (self): + return u'bass-alter' + +class ChordModification (Music_xml_node): + def get_type (self): + ch = self.get_maybe_exist_typed_child (get_class (u'degree-type')) + return {'add': 1, 'alter': 1, 'subtract': -1}.get (ch.get_text ().strip (), 0) + def get_value (self): + ch = self.get_maybe_exist_typed_child (get_class (u'degree-value')) + value = 0 + if ch: + value = int (ch.get_text ().strip ()) + return value + def get_alter (self): + ch = self.get_maybe_exist_typed_child (get_class (u'degree-alter')) + return interpret_alter_element (ch) + + class Frame (Music_xml_node): def get_frets (self): return self.get_named_child_value_number ('frame-frets', 4) @@ -866,6 +1114,7 @@ class Frame (Music_xml_node): return self.get_named_child_value_number ('frame-strings', 6) def get_first_fret (self): return self.get_named_child_value_number ('first-fret', 1) + class Frame_Note (Music_xml_node): def get_string (self): return self.get_named_child_value_number ('string', 1) @@ -880,6 +1129,25 @@ class Frame_Note (Music_xml_node): else: return '' +class FiguredBass (Music_xml_node): + pass + +class Beats (Music_xml_node): + pass + +class BeatType (Music_xml_node): + pass + +class BeatUnit (Music_xml_node): + pass + +class BeatUnitDot (Music_xml_node): + pass + +class PerMinute (Music_xml_node): + pass + + ## need this, not all classes are instantiated ## for every input file. Only add those classes, that are either directly @@ -891,21 +1159,33 @@ class_dict = { 'attributes': Attributes, 'barline': Barline, 'bar-style': BarStyle, + 'bass': Bass, 'beam' : Beam, + 'beats': Beats, + 'beat-type': BeatType, + 'beat-unit': BeatUnit, + 'beat-unit-dot': BeatUnitDot, 'bend' : Bend, 'bracket' : Bracket, 'chord': Chord, 'dashes' : Dashes, + 'degree' : ChordModification, 'dot': Dot, 'direction': Direction, 'direction-type': DirType, 'duration': Duration, + 'elision': Elision, + 'extend': Extend, 'frame': Frame, 'frame-note': Frame_Note, + 'figured-bass': FiguredBass, 'glissando': Glissando, 'grace': Grace, 'harmony': Harmony, 'identification': Identification, + 'key-alter': KeyAlter, + 'key-octave': KeyOctave, + 'key-step': KeyStep, 'lyric': Lyric, 'measure': Measure, 'notations': Notations, @@ -915,9 +1195,11 @@ class_dict = { 'part-group': Part_group, 'part-list': Part_list, 'pedal': Pedal, + 'per-minute': PerMinute, 'pitch': Pitch, 'rest': Rest, - 'score-part': Score_part, + 'root': Root, + 'score-part': Score_part, 'slide': Slide, 'slur': Slur, 'staff': Staff, @@ -953,7 +1235,8 @@ def get_class (name): def lxml_demarshal_node (node): name = node.tag - if name is None: + # Ignore comment nodes, which are also returned by the etree parser! + if name is None or node.__class__.__name__ == "_Comment": return None klass = get_class (name) py_node = klass() @@ -967,7 +1250,7 @@ def lxml_demarshal_node (node): for c in py_node._children: c._parent = py_node - for (k,v) in node.items (): + for (k, v) in node.items (): py_node.__dict__[k] = v py_node._attribute_dict[k] = v @@ -977,14 +1260,14 @@ def minidom_demarshal_node (node): name = node.nodeName klass = get_class (name) - py_node = klass() + py_node = klass () py_node._name = name py_node._children = [minidom_demarshal_node (cn) for cn in node.childNodes] for c in py_node._children: c._parent = py_node if node.attributes: - for (nm, value) in node.attributes.items(): + for (nm, value) in node.attributes.items (): py_node.__dict__[nm] = value py_node._attribute_dict[nm] = value @@ -997,10 +1280,10 @@ def minidom_demarshal_node (node): if __name__ == '__main__': - import lxml.etree + import lxml.etree - tree = lxml.etree.parse ('beethoven.xml') - mxl_tree = lxml_demarshal_node (tree.getroot ()) - ks = class_dict.keys() - ks.sort() - print '\n'.join (ks) + tree = lxml.etree.parse ('beethoven.xml') + mxl_tree = lxml_demarshal_node (tree.getroot ()) + ks = class_dict.keys () + ks.sort () + print '\n'.join (ks)