diff --git a/README.rst b/README.rst index 6b5309d..715c13c 100644 --- a/README.rst +++ b/README.rst @@ -44,14 +44,13 @@ mscxyz .. code-block:: text - usage: mscx-manager [-h] [-V] [-b] [-c] [-C GENERAL_CONFIG_FILE] [-d] [-g GENERAL_GLOB] [-m] [-e GENERAL_EXECUTABLE] [-v] - {clean,meta,lyrics,rename,export,help} ... path + usage: mscx-manager [-h] [-V] [-b] [-c] [-C GENERAL_CONFIG_FILE] [-d] [-g GENERAL_GLOB] [-m] [-e GENERAL_EXECUTABLE] [-v] {clean,meta,lyrics,rename,export,help} ... path A command line tool to manipulate the XML based "*.mscX" and "*.mscZ" files of the notation software MuseScore. positional arguments: - path Path to a "*.mscx" file or a folder which contains "*.mscx" files. In conjunction with the subcommand "help" this positional - parameter accepts the names of all other subcommands or the word "all". + path Path to a "*.mscx" file or a folder which contains "*.mscx" files. In conjunction with the subcommand "help" this positional parameter accepts the names of + all other subcommands or the word "all". options: -h, --help show this help message and exit @@ -62,8 +61,8 @@ mscxyz Specify a configuration file in the INI format. -d, --dry-run Simulate the actions. -g GENERAL_GLOB, --glob GENERAL_GLOB - Handle only files which matches against Unix style glob patterns (e. g. "*.mscx", "* - *"). If you omit this option, the standard - glob pattern "*.msc[xz]" is used. + Handle only files which matches against Unix style glob patterns (e. g. "*.mscx", "* - *"). If you omit this option, the standard glob pattern "*.msc[xz]" is + used. -m, --mscore Open and save the XML file in MuseScore after manipulating the XML with lxml to avoid differences in the XML structure. -e GENERAL_EXECUTABLE, --executable GENERAL_EXECUTABLE Path of the musescore executable. @@ -74,12 +73,12 @@ mscxyz Run "subcommand --help" for more informations. clean Clean and reset the formating of the "*.mscx" file meta Deal with meta data informations stored in the MuseScore file. - lyrics Extract lyrics. Without any option this subcommand extracts all lyrics verses into separate mscx files. This generated mscx files - contain only one verse. The old verse number is appended to the file name, e. g.: score_1.mscx. + lyrics Extract lyrics. Without any option this subcommand extracts all lyrics verses into separate mscx files. This generated mscx files contain only one verse. The + old verse number is appended to the file name, e. g.: score_1.mscx. rename Rename the "*.mscx" files. export Export the scores to PDFs or to the specified extension. - help Show help. Use “mscx-manager help all” to show help messages of all subcommands. Use “mscx-manager help ” to show only - help messages for the given subcommand. + help Show help. Use “mscx-manager help all” to show help messages of all subcommands. Use “mscx-manager help ” to show only help messages for the + given subcommand. Subcommands =========== @@ -101,8 +100,7 @@ mscx-manager meta .. code-block:: text - usage: mscx-manager meta [-h] [-c META_CLEAN] [-D] [-d SOURCE_FIELDS FORMAT_STRING] [-j] [-l DESTINATION FORMAT_STRING] [-s] - [-S DESTINATION_FIELD FORMAT_STRING] + usage: mscx-manager meta [-h] [-c META_CLEAN] [-D] [-d SOURCE_FIELDS FORMAT_STRING] [-j] [-l DESTINATION FORMAT_STRING] [-s] [-S DESTINATION_FIELD FORMAT_STRING] MuseScore can store meta data informations in different places: @@ -183,18 +181,16 @@ mscx-manager meta -c META_CLEAN, --clean META_CLEAN Clean the meta data fields. Possible values: „all“ or „field_one,field_two“. -D, --delete-duplicates - Deletes combined_lyricist if this field is equal to combined_composer. Deletes combined_subtitle if this field is equal - tocombined_title. Move combined_subtitle to combimed_title if combined_title is empty. + Deletes combined_lyricist if this field is equal to combined_composer. Deletes combined_subtitle if this field is equal tocombined_title. Move + combined_subtitle to combimed_title if combined_title is empty. -d SOURCE_FIELDS FORMAT_STRING, --distribute-fields SOURCE_FIELDS FORMAT_STRING - Distribute source fields to target fields applying a format string on the source fields. It is possible to apply multiple - --distribute-fields options. SOURCE_FIELDS can be a single field or a comma separated list of fields: field_one,field_two. The - program tries first to match the FORMAT_STRING on the first source field. If this fails, it tries the second source field ... an so - on. + Distribute source fields to target fields applying a format string on the source fields. It is possible to apply multiple --distribute-fields options. + SOURCE_FIELDS can be a single field or a comma separated list of fields: field_one,field_two. The program tries first to match the FORMAT_STRING on the first + source field. If this fails, it tries the second source field ... an so on. -j, --json Additionally write the meta data to a json file. -l DESTINATION FORMAT_STRING, --log DESTINATION FORMAT_STRING Write one line per file to a text file. e. g. --log /tmp/mscx-manager.log '$title $composer' - -s, --synchronize Synchronize the values of the first vertical frame (vbox) (title, subtitle, composer, lyricist) with the corresponding metadata - fields + -s, --synchronize Synchronize the values of the first vertical frame (vbox) (title, subtitle, composer, lyricist) with the corresponding metadata fields -S DESTINATION_FIELD FORMAT_STRING, --set-field DESTINATION_FIELD FORMAT_STRING Set value to meta data fields. @@ -210,9 +206,8 @@ mscx-manager lyrics -e LYRICS_EXTRACT, --extract LYRICS_EXTRACT The lyric verse number to extract or "all". -r LYRICS_REMAP, --remap LYRICS_REMAP - Remap lyrics. Example: "--remap 3:2,5:3". This example remaps lyrics verse 3 to verse 2 and verse 5 to 3. Use commas to specify - multiple remap pairs. One remap pair is separated by a colon in this form: "old:new": "old" stands for the old verse number. "new" - stands for the new verse number. + Remap lyrics. Example: "--remap 3:2,5:3". This example remaps lyrics verse 3 to verse 2 and verse 5 to 3. Use commas to specify multiple remap pairs. One + remap pair is separated by a colon in this form: "old:new": "old" stands for the old verse number. "new" stands for the new verse number. -f, --fix Fix lyrics: Convert trailing hyphens ("la- la- la") to a correct hyphenation ("la - la - la") mscx-manager rename diff --git a/docs/cli.rst b/docs/cli.rst index d9c56ca..a9b8b02 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -7,14 +7,13 @@ mscxyz .. code-block:: text - usage: mscx-manager [-h] [-V] [-b] [-c] [-C GENERAL_CONFIG_FILE] [-d] [-g GENERAL_GLOB] [-m] [-e GENERAL_EXECUTABLE] [-v] - {clean,meta,lyrics,rename,export,help} ... path + usage: mscx-manager [-h] [-V] [-b] [-c] [-C GENERAL_CONFIG_FILE] [-d] [-g GENERAL_GLOB] [-m] [-e GENERAL_EXECUTABLE] [-v] {clean,meta,lyrics,rename,export,help} ... path A command line tool to manipulate the XML based "*.mscX" and "*.mscZ" files of the notation software MuseScore. positional arguments: - path Path to a "*.mscx" file or a folder which contains "*.mscx" files. In conjunction with the subcommand "help" this positional - parameter accepts the names of all other subcommands or the word "all". + path Path to a "*.mscx" file or a folder which contains "*.mscx" files. In conjunction with the subcommand "help" this positional parameter accepts the names of + all other subcommands or the word "all". options: -h, --help show this help message and exit @@ -25,8 +24,8 @@ mscxyz Specify a configuration file in the INI format. -d, --dry-run Simulate the actions. -g GENERAL_GLOB, --glob GENERAL_GLOB - Handle only files which matches against Unix style glob patterns (e. g. "*.mscx", "* - *"). If you omit this option, the standard - glob pattern "*.msc[xz]" is used. + Handle only files which matches against Unix style glob patterns (e. g. "*.mscx", "* - *"). If you omit this option, the standard glob pattern "*.msc[xz]" is + used. -m, --mscore Open and save the XML file in MuseScore after manipulating the XML with lxml to avoid differences in the XML structure. -e GENERAL_EXECUTABLE, --executable GENERAL_EXECUTABLE Path of the musescore executable. @@ -37,12 +36,12 @@ mscxyz Run "subcommand --help" for more informations. clean Clean and reset the formating of the "*.mscx" file meta Deal with meta data informations stored in the MuseScore file. - lyrics Extract lyrics. Without any option this subcommand extracts all lyrics verses into separate mscx files. This generated mscx files - contain only one verse. The old verse number is appended to the file name, e. g.: score_1.mscx. + lyrics Extract lyrics. Without any option this subcommand extracts all lyrics verses into separate mscx files. This generated mscx files contain only one verse. The + old verse number is appended to the file name, e. g.: score_1.mscx. rename Rename the "*.mscx" files. export Export the scores to PDFs or to the specified extension. - help Show help. Use “mscx-manager help all” to show help messages of all subcommands. Use “mscx-manager help ” to show only - help messages for the given subcommand. + help Show help. Use “mscx-manager help all” to show help messages of all subcommands. Use “mscx-manager help ” to show only help messages for the + given subcommand. Subcommands =========== @@ -64,8 +63,7 @@ mscx-manager meta .. code-block:: text - usage: mscx-manager meta [-h] [-c META_CLEAN] [-D] [-d SOURCE_FIELDS FORMAT_STRING] [-j] [-l DESTINATION FORMAT_STRING] [-s] - [-S DESTINATION_FIELD FORMAT_STRING] + usage: mscx-manager meta [-h] [-c META_CLEAN] [-D] [-d SOURCE_FIELDS FORMAT_STRING] [-j] [-l DESTINATION FORMAT_STRING] [-s] [-S DESTINATION_FIELD FORMAT_STRING] MuseScore can store meta data informations in different places: @@ -146,18 +144,16 @@ mscx-manager meta -c META_CLEAN, --clean META_CLEAN Clean the meta data fields. Possible values: „all“ or „field_one,field_two“. -D, --delete-duplicates - Deletes combined_lyricist if this field is equal to combined_composer. Deletes combined_subtitle if this field is equal - tocombined_title. Move combined_subtitle to combimed_title if combined_title is empty. + Deletes combined_lyricist if this field is equal to combined_composer. Deletes combined_subtitle if this field is equal tocombined_title. Move + combined_subtitle to combimed_title if combined_title is empty. -d SOURCE_FIELDS FORMAT_STRING, --distribute-fields SOURCE_FIELDS FORMAT_STRING - Distribute source fields to target fields applying a format string on the source fields. It is possible to apply multiple - --distribute-fields options. SOURCE_FIELDS can be a single field or a comma separated list of fields: field_one,field_two. The - program tries first to match the FORMAT_STRING on the first source field. If this fails, it tries the second source field ... an so - on. + Distribute source fields to target fields applying a format string on the source fields. It is possible to apply multiple --distribute-fields options. + SOURCE_FIELDS can be a single field or a comma separated list of fields: field_one,field_two. The program tries first to match the FORMAT_STRING on the first + source field. If this fails, it tries the second source field ... an so on. -j, --json Additionally write the meta data to a json file. -l DESTINATION FORMAT_STRING, --log DESTINATION FORMAT_STRING Write one line per file to a text file. e. g. --log /tmp/mscx-manager.log '$title $composer' - -s, --synchronize Synchronize the values of the first vertical frame (vbox) (title, subtitle, composer, lyricist) with the corresponding metadata - fields + -s, --synchronize Synchronize the values of the first vertical frame (vbox) (title, subtitle, composer, lyricist) with the corresponding metadata fields -S DESTINATION_FIELD FORMAT_STRING, --set-field DESTINATION_FIELD FORMAT_STRING Set value to meta data fields. @@ -173,9 +169,8 @@ mscx-manager lyrics -e LYRICS_EXTRACT, --extract LYRICS_EXTRACT The lyric verse number to extract or "all". -r LYRICS_REMAP, --remap LYRICS_REMAP - Remap lyrics. Example: "--remap 3:2,5:3". This example remaps lyrics verse 3 to verse 2 and verse 5 to 3. Use commas to specify - multiple remap pairs. One remap pair is separated by a colon in this form: "old:new": "old" stands for the old verse number. "new" - stands for the new verse number. + Remap lyrics. Example: "--remap 3:2,5:3". This example remaps lyrics verse 3 to verse 2 and verse 5 to 3. Use commas to specify multiple remap pairs. One + remap pair is separated by a colon in this form: "old:new": "old" stands for the old verse number. "new" stands for the new verse number. -f, --fix Fix lyrics: Convert trailing hyphens ("la- la- la") to a correct hyphenation ("la - la - la") mscx-manager rename diff --git a/docs/index.rst b/docs/index.rst index f55ac63..5cad435 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ Welcome to mscxyz's documentation! Contents: .. toctree:: - :maxdepth: 2 + :maxdepth: 3 cli modules diff --git a/docs/modules.rst b/docs/modules.rst index d26382d..8579305 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -39,7 +39,18 @@ mscxyz.settings module .. automodule:: mscxyz.settings +mscxyz.style +^^^^^^^^^^^^ + +.. automodule:: mscxyz.style + mscxyz.utils module ^^^^^^^^^^^^^^^^^^^ .. automodule:: mscxyz.utils + + +mscxyz.xml module +^^^^^^^^^^^^^^^^^ + +.. automodule:: mscxyz.xml \ No newline at end of file diff --git a/mscxyz/style.py b/mscxyz/style.py index 5f1e286..f729ae9 100644 --- a/mscxyz/style.py +++ b/mscxyz/style.py @@ -7,7 +7,7 @@ import lxml.etree from lxml.etree import _Element -from mscxyz.xml import find_safe +from mscxyz.xml import find_safe, xpath if typing.TYPE_CHECKING: from mscxyz.score_file_classes import MuseScoreFile @@ -57,7 +57,7 @@ def _create(self, tag: str) -> _Element: def get_element(self, element_path: str, create: bool = False) -> _Element: """ - Determines an lxml element that is parent to the ``style`` tag + Determines an lxml element that is a child to the ``Style`` tag :param element_path: see http://lxml.de/tutorial.html#elementpath @@ -119,9 +119,16 @@ def _get_text_style_element(self, name: str) -> _Element: raise ValueError( "This operation is only allowed for MuseScore 2 score files" ) - child = self.score.xml_tree.xpath(f'//TextStyle/name[contains(., "{name}")]') - if child: - return child[0].getparent() + + child: _Element | None = xpath( + self.score.xml_root, f'//TextStyle/name[contains(., "{name}")]' + ) + + if child is not None: + el: _Element | None = child.getparent() + if el is None: + raise ValueError(f"Parent not found on element {el}!") + return el else: el_text_style: _Element = lxml.etree.SubElement(self.element, "TextStyle") el_name: _Element = lxml.etree.SubElement(el_text_style, "name") @@ -129,21 +136,47 @@ def _get_text_style_element(self, name: str) -> _Element: return el_text_style def get_text_style(self, name: str) -> dict[str, str]: - """Get text styles. Only MuseScore2! + """Get text styles as a dictonary. Only MuseScore2! - :param name: The name of the text style. + .. code :: XML + + + + .. code :: Python + + { + "halign": "center", + "size": "28", + "family": "MuseJazz", + "bold": "1", + "valign": "top", + "name": "Title", + "offsetType": "absolute", + } + + :param name: The name of the text style, for example ``Title``, ``Subtitle``, ``Lyricist``, ``Fingering``, ``String Number``, ``Dynamics`` etc. """ text_style = self._get_text_style_element(name) out: dict[str, str] = {} for child in text_style.iterchildren(): - out[child.tag] = child.text + if child.text is not None: + out[child.tag] = child.text return out def set_text_style(self, name: str, values: dict[str, str | int | float]) -> None: """Set text styles. Only MuseScore2! - :param name: The name of the text style. - :param values: A dictionary. The keys are the tag names, values are + :param name: The name of the text style, for example ``Title``, ``Subtitle``, ``Lyricist``, ``Fingering``, ``String Number``, ``Dynamics`` etc. :param values: A dictionary. The keys are the tag names, values are the text values of the child tags, for example ``{size: 14, bold: 1}``. """ diff --git a/mscxyz/xml.py b/mscxyz/xml.py index 8767986..9fdb3c6 100644 --- a/mscxyz/xml.py +++ b/mscxyz/xml.py @@ -17,6 +17,12 @@ def find_safe(element: _Element, path: str) -> _Element: return result +def xpath(element: _Element, path: str) -> _Element | None: + output: list[_Element] | None = xpathall(element, path) + if output and len(output) > 0: + return output[0] + + def xpath_safe(element: _Element, path: str) -> _Element: output: list[_Element] = xpathall_safe(element, path) if len(output) > 1: @@ -24,15 +30,21 @@ def xpath_safe(element: _Element, path: str) -> _Element: return output[0] -def xpathall_safe(element: _Element, path: str) -> list[_Element]: +def xpathall(element: _Element, path: str) -> list[_Element] | None: result: _XPathObject = element.xpath(path) output: list[_Element] = [] + if isinstance(result, list): for item in result: if isinstance(item, _Element): output.append(item) - if len(output) == 0: - raise ValueError(f"XPath “{path}” not found in element {element}!") + if len(output) > 0: + return output + +def xpathall_safe(element: _Element, path: str) -> list[_Element]: + output: list[_Element] | None = xpathall(element, path) + if output is None: + raise ValueError(f"XPath “{path}” not found in element {element}!") return output diff --git a/tests/test_xml.py b/tests/test_xml.py index ecdeca1..ab2f574 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -4,7 +4,7 @@ import pytest -from mscxyz.xml import find_safe, xpath_safe, xpathall_safe +from mscxyz.xml import find_safe, xpath, xpath_safe, xpathall, xpathall_safe from tests import helper tree = helper.get_xml_tree("simple.mscz", 4) @@ -17,6 +17,11 @@ def test_find_safe(): assert element.tag == "Score" +def test_xpath(): + element = xpath(root, ".//xxxxxxx") + assert element is None + + class TestXpathSave: def test_xpath_safe(self): element = xpath_safe(root, ".//Score") @@ -28,6 +33,11 @@ def test_xpath_safe_raise(self): assert "XPath “.//metaTag” found more than one element in" in e.value.args[0] +def test_xpathall(): + element = xpathall(root, ".//xxxxxxx") + assert element is None + + def test_xpathall_safe(): element = xpathall_safe(root, ".//metaTag") assert isinstance(element, list)