nCoda Lychee Docs

Document Module Internal Interface


This module is intended for internal Lychee use only, so the API may change without notice. If you wish to use this module outside Lychee, please contact us to discuss the best way.


The private functions and methods documented here should not be used outside this module, even for other Lychee modules. If you wish to use a private function or method, please either find a way to use a public function or method, or consider creating such a public function or method.

The Document class represents a Lychee-MEI document. In order to ensure compliance with the Lychee-MEI specification, we recommend using Document whenever possible to avoid duplicating functionality (improperly).

The Lychee-MEI specification is described in Lychee-MEI (LMEI). You can find information about supported Lychee-MEI metadata headers in MEI Headers.

class lychee.document.document.Document(repository_path=None)[source]

Object representing an MEI document. Use methods prefixed with get to obtain portions of the document, automatically loading from files if required. Use methods prefixed with put to submit a new portion to replace the existing portion outright.

If you do not provide a repository_path argument to the initialization method, no files will be written. Additionally, the save_everything() method therefore will not work, and all get_() methods will not return useful data until they have been given data.


When you use a put_() method, the element(s) replace those already present in this Document instance, but they will not be written to the document’s directory until you call save_everything() or the context manager exits, if applicable.

The recommended way to use a Document with file output is as a context manager (using a with statement). This way, you cannot forget to save your changes to the filesystem.

_APPROVED_HEAD_ELEMENTS = ('fileDesc', 'titleStmt', 'title', 'respStmt', 'arranger', 'author', 'composer', 'editor', 'funder', 'librettist', 'lyricist', 'sponsor', 'pubStmt')
__dict__ = dict_proxy({'__module__': 'lychee.document.document', 'get_section': <function get_section>, '__exit__': <function __exit__>, 'get_head': <function get_head>, 'put_score': <function put_score>, 'move_section_to': <function move_section_to>, 'get_section_ids': <function get_section_ids>, 'get_score': <function get_score>, 'put_section': <function put_section>, 'put_in_head': <function put_in_head>, '__dict__': <attribute '__dict__' of 'Document' objects>, 'save_everything': <function save_everything>, '__weakref__': <attribute '__weakref__' of 'Document' objects>, '__init__': <function __init__>, 'get_from_head': <function get_from_head>, '_APPROVED_HEAD_ELEMENTS': ('fileDesc', 'titleStmt', 'title', 'respStmt', 'arranger', 'author', 'composer', 'editor', 'funder', 'librettist', 'lyricist', 'sponsor', 'pubStmt'), '__enter__': <function __enter__>, 'put_head': <function put_head>, '__doc__': "\n Object representing an MEI document. Use methods prefixed with ``get`` to obtain portions of\n the document, automatically loading from files if required. Use methods prefixed with ``put``\n to submit a new portion to *replace* the existing portion outright.\n\n If you do not provide a ``repository_path`` argument to the initialization method, no files will\n be written. Additionally, the :meth:`save_everything` method therefore will not work, and all\n :meth:`get_` methods will not return useful data until they have been given data.\n\n .. note:: When you use a :meth:`put_` method, the element(s) replace those already present in\n this :class:`Document` instance, but they will not be written to the document's directory\n until you call :meth:`save_everything` or the context manager exits, if applicable.\n\n The recommended way to use a :class:`Document` with file output is as a context manager (using\n a :obj:`with` statement). This way, you cannot forget to save your changes to the filesystem.\n "})

Start a context manager for Document.

__exit__(exc_type, exc_val, exc_tb)[source]

Save all sections into files and stuff.

Parameters:repository_path (str) – Path to a directory in which the files for this Document are or will be stored. The default of None will not save any files.
__module__ = 'lychee.document.document'

list of weak references to the object (if defined)


Getter for elements in the <meiHead>.

Parameters:what (str) – The element name to find. See the list of valid values below.
Returns:A list of the requested elements.
Return type:list of lxml.etree.Element

You may request the following elements:

  • fileDesc
  • titleStmt
  • title
  • respStmt
  • a role (arranger, author, composer, editor, funder, librettist, lyricist, or sponsor)
  • pubStmt

The “respStmt” child elements describe Lychee users who have edited this document, whether or not they hold a more specific role.

Also note that, if a role-specific element (such as <composer>) corresponds to a Lychee user, the role-specific element will contain a <persName> with a @nymref attribute that holds the @xml:id value of a <persName> given in the <respStmt>. For example, if this work’s composer is also the only person who has edited this score:

>>> etree.dump(doc.get_from_head('composer')[0])
    <persName nymref="#p1234"/>
>>> etree.dump(doc.get_from_head('respStmt')[0])
    <persName xml:id="p1234">
        <persName type="full">Danceathon Smith</persName>


At this point in time, get_from_head() does not raise (its own) exceptions. If the “what” argument is invalid, this is treated the same as a missing element, so the return value will be None.


Load and return the MEI header metadata.

Returns:The <meiHead> portion of the MEI document.
Return type:lxml.etree.Element
Raises:lychee.exceptions.HeaderNotFoundError if the <meiHead> element is missing or it contains a <ptr> without a @target attribute

Load and return the whole score, excluding metadata and “inactive” <section> elements.

Returns:A <score> element with relevant <section> elements in the proper order.
Return type:lxml.etree.Element
Raises:lychee.exceptions.SectionNotFoundError if one or more of the <section> elements require for the <score> cannot be found.

Side Effect

Caches the returned <score> for later access.


Load and return a section of the score.

Returns:The section with an @xml:id matching section_id.
Return type:lxml.etree.Element
Raises:lychee.exceptions.SectionNotFoundError if no <section> with the specified @xml:id can be found.
Raises:lychee.exceptions.InvalidFileError if the <section> is found but cannot be loaded because it is invalid.

Side Effects

If the section is not already loaded, get_section() will try to fetch it from the filesystem, if a repository is configured.


By default, return the ordered @xml:id attributes of active <section> elements in this score. If all_sections is True, return the @xml:id attributes of all <section> elements in this MEI document, in arbitrary order.

Parameters:all_sections (bool) – Whether to return the IDs of all sections in this document, rather than just the sections currently active in the score.
Returns:A list of the section IDs.
Return type:list of str
move_section_to(xmlid, position)[source]

Move a <section> to another position in the score.

  • xmlid (string) – The @xml:id attribute of the <section> to move. The section may or may not already be in the active score, but it must already be part of the document.
  • position (int) – The requested new index of the section in the active score.

SectionNotFoundError if the <section> to move is not found in the repository.

Note that this function simply moves the indicated section to the requested position, but does not save the Document instance or anything else (e.g., does not cause an outbound conversion that would update code external to Lychee).


Save new header metadata.

Param:new_head: An <meiHead> element that should replace the existing one.

As per get_from_head(), but with setting instead.


This method is not implemented. Refer to T109 for more information.


Save a new score in place of the existing one.

Parameters:new_music (lxml.etree.Element) – The <score> element to use in place of the existing one.
Returns:The @xml:id attributes of all top-level <section> elements found in new_music, in the order they were found.
Return type:list of str

The “score order” in new_music replaces the existing “score order.” Note that existing <section> elements that aren’t part of new_music are not deleted—they remain tracked internally. Also note that any <section> elements already contained in the local list of sections is replaced by the section that has a matching @xml:id in new_music. Sections that don’t have an @xml:id will be given one.


Add or replace a <section> in the current MEI document.

Parameters:new_section (lxml.etree.Element) – A new section or a replacement for an existing section with the same @xml:id attribute.
Returns:The @xml:id of the saved new_section.
Return type:str


If new_section is missing an @xml:id attribute, or has an invalid @xml:id attribute, a new one is created.


Write the MEI document(s) into files.

Returns:A list of the absolute pathnames that are part of this Lychee-MEI document.
Return type:list of str
Raises:lychee.exceptions.CannotSaveError if the document cannot be written to the filesystem (this happens when repository_path was not supplied on initialization).

A Lychee-MEI document is a complex of various XML elements. This method arranges for the documents stored in memory to be saved into files in the proper arrangement as specified by the order.

Note that the return value includes any file in the document. The files may not have been modified, and in fact may not even have been saved at all—they are simply part of this document.


Determine whether “xmlid” is a valid Lychee-MEI @xml:id value for a <section>.

Parameters:xmlid (str) – The @xml:id value to check.
Returns:Whether “xmlid” is valid.
Return type:bool

Check the @ly:version attribute on the root element of an LMEI ElementTree.

Parameters:lmei (lxml.etree.ElementTree) – An LMEI ElementTree to check.
Returns:The unmodified LMEI document.
Return type:lxml.etree.ElementTree
Raises:exceptions.LycheeMEIError if the file is too old and not supported.

Currently, this function checks whether the @ly:version attribute is present on the root element of the ElementTree given, and:

  • if @ly:version is missing, prints an “error” log message
  • if @ly:version is not a proper “semantic versioning” string, prints an “error” log message
  • if @ly:version is the same as the version of this Lychee, continues silently

In addition, the function checks for incompatible versions:

  • if @ly:version indicates a future version, prints a “warning” log message
  • if @ly:version is older than 0.6.0, raises a LycheeMEIError


The patch release is ignored for the compatibility check.


Ensure the only characters in a string are acceptable as an @xml:id.

At the moment, this only checks that a " (double-quote) character is not in the string. If I can figure out what the XML:ID spec really means, then it may be restricted further… but it doesn’t seem to be the case!

Parameters:xmlid (str) – The @xml:id string to check.
Returns:True if all the characters are valid in an @xml:id string, otherwise False.
lychee.document.document._ensure_score_order(score, order)[source]

Ensure there are <section> elements in score with the same @xml:id attributes, in the same order, as they appear in order.

  • score (lxml.etree.Element) – The <score> element in which to inspect the <section> elements.
  • order (list of str) – List of the expected @xml:id attribute values, in the order desired.

Whether the desired <section> elements are in the proper order.

Return type:



>>> score_tag = '{}score'
>>> section_tag = '{}section'
>>> xmlid_tag = '{}id'
>>> from lxml import etree
>>> score = etree.Element(score_tag)
>>> score.append(etree.Element(section_tag, attrib={xmlid_tag: '123'}))
>>> score.append(etree.Element(section_tag, attrib={xmlid_tag: '456'}))
>>> score.append(etree.Element(section_tag, attrib={xmlid_tag: '789'}))
>>> _ensure_score_order(score, ['123', '456', '789'])
>>> _ensure_score_order(score, ['123', '789'])
>>> _ensure_score_order(score, ['123', '789', '456'])
>>> _ensure_score_order(score, ['123', '234', '456', '789'])

Initialize a dictionary with keys corresponding to all the <section> elements in the document, but the keys set to None. In effect, this is enough information to know which sections are in a document, but does not spend time loading and caching the sections.

Parameters:all_files (lxml.etree._Element) – The “all_files” <meiCorpus> element from which to load sections.
Returns:A dictionary with keys as @xml:id of every <section>.
Return type:dict
Raises:lychee.exceptions.InvalidDocumentError if the @target attribute of a <ptr> to a <section> does not end with ".mei".
lychee.document.document._load_in(from_here, recover=None)[source]

Try to load an MEI/XML file at the path from_here.

  • from_here (str) – The pathname from which to try parsing a file.
  • recover (bool) – If True, the XML document will be parsed with a parser object set to “recover,” which tries “hard to parse through broken XML.” Default is False. Generally, this should be avoided—callers should make their users aware that they’re entering some parallel universe when their XML is broken.

The MEI/XML document stored at from_here.

Return type:



exceptions.FileNotFoundError if the file does not exist, is not readable, is a directory, or something like that.


exceptions.InvalidFileError if the file exists and can be loaded, but lxml cannot parse a valid XML document from it.

lychee.document.document._load_score_order(repo_path, all_files)[source]

Determine the proper “self._score_order” instance variable from the local repository.

  • repo_path (str) – The repository’s directory path.
  • all_files (lxml.etree._Element) – The “all_files” document from which to determine the score order.

An ordered list of the @xml:id attributes of <section> elements in the active score.

Return type:

list of str


lychee.exceptions.InvalidFileError if the <ptr> elements are malformed

This method requires that “self._all_files” exists. If this is the default returned by _make_empty_all_files(), an empty list is returned. Otherwise, the “score” <ptr> is loaded, and the returned value is the @target attributes of contained <ptr> elements for which @targettype=”section”, but without the terminating “.mei” part of the filename. In other words, if the repository contains a compliant Lychee-MEI file, this method returns a list of the @xml:id of <section> elements in the active score.


Produce and return an empty all_files.mei file that will be used to cross-reference all other files in this repository.

Parameters:pathname (str) – The pathname to use for the file—must include the “all_files.mei” part. If pathname is None, the file will not be saved.
Returns:The XML document produced.
Return type:lxml.etree.ElementTree

Produce an “empty” <meiHead> element.

Returns:The <meiHead> element.
Return type:lxml.etree.Element

The output produced is not truly “empty.” It contains the minimal information required:

                <title type="main">(Untitled)</title>
            <unpub>This is an unpublished Lychee-MEI document.</unpub>
lychee.document.document._make_ptr(targettype, target)[source]

Make a <ptr> element with the specified targettype and target attributes.

  • targettype (str) – The value of the @targettype attribute.
  • target (str) – The value of the @target attribute.

The <ptr>.

Return type:


lychee.document.document._save_out(this, to_here)[source]

Take this Element or ElementTree and save to_here.

  • this (lxml.etree.Element or lxml.etree.ElementTree) – An element (tree) to save to a file.
  • to_here (str) – The pathname in which to save the file.



lychee.exceptions.CannotSaveError if something messes up

lychee.document.document._set_default(here, this, that)[source]

Returns here[this], if this is a key in here, otherwise returns that.


>>> _set_default({'a': 12}, 'a', 42)
>>> _set_default({'a': 12}, 'b', 42)

Produce a string of seven pseudo-random digits.

Returns:A string with seven pseudo-random digits.
Return type:str


The first character will never be 0, so you can rely on the output from this function being greater than or equal to one million, and strictly less than ten million.