diff --git a/mscxyz/__init__.py b/mscxyz/__init__.py index efb1533..c3b80cb 100644 --- a/mscxyz/__init__.py +++ b/mscxyz/__init__.py @@ -7,8 +7,8 @@ .. code :: - MscoreFile - MscoreXmlTree + MuseScoreFile + MuseScoreFile MscoreLyricsInterface MscoreMetaInterface MscoreStyleInterface @@ -37,9 +37,8 @@ from mscxyz.meta import Meta from mscxyz.rename import rename_filename from mscxyz.score_file_classes import ( - MscoreFile, MscoreStyleInterface, - MscoreXmlTree, + MuseScoreFile, list_scores, ) from mscxyz.settings import DefaultArguments @@ -54,17 +53,10 @@ # Classes -# Level 1 -MscoreFile +MuseScoreFile """see submodule ``score_file_classes.py``""" -# Level 2 - -MscoreXmlTree -"""see submodule ``score_file_classes.py``""" - -# Level 3 MscoreLyricsInterface """see submodule ``lyrics.py``""" @@ -216,13 +208,11 @@ def execute(args: typing.Sequence[str] | None = None): print("\n" + color(file, "red")) if args.general_backup: - from mscxyz.score_file_classes import MscoreFile - - score = MscoreFile(file) + score = MuseScoreFile(file) score.backup() if args.subcommand == "clean": - score = MscoreXmlTree(file) + score = MuseScoreFile(file) print(score.filename) score.clean() if args.clean_style: @@ -269,9 +259,7 @@ def execute(args: typing.Sequence[str] | None = None): score = rename_filename(file) elif args.subcommand == "export": - from mscxyz.score_file_classes import MscoreFile - - score = MscoreFile(file) + score = MuseScoreFile(file) score.export(extension=args.export_extension) report_errors(score.errors) diff --git a/mscxyz/lyrics.py b/mscxyz/lyrics.py index 2168505..67e468a 100644 --- a/mscxyz/lyrics.py +++ b/mscxyz/lyrics.py @@ -4,10 +4,10 @@ import lxml.etree as etree -from mscxyz.score_file_classes import MscoreXmlTree +from mscxyz.score_file_classes import MuseScoreFile -class MscoreLyricsInterface(MscoreXmlTree): +class MscoreLyricsInterface(MuseScoreFile): def __init__(self, relpath: str): super(MscoreLyricsInterface, self).__init__(relpath) self.lyrics = self.normalize_lyrics() diff --git a/mscxyz/meta.py b/mscxyz/meta.py index 5826605..6e7975e 100644 --- a/mscxyz/meta.py +++ b/mscxyz/meta.py @@ -11,7 +11,7 @@ import tmep from lxml.etree import _Element -from mscxyz.score_file_classes import MscoreXmlTree +from mscxyz.score_file_classes import MuseScoreFile from mscxyz.utils import color, get_args if typing.TYPE_CHECKING: @@ -286,7 +286,7 @@ def __setattr__(self, field, value): self._set_text(field.title(), value) -class Combined(MscoreXmlTree): +class Combined(MuseScoreFile): fields = ( "composer", "lyricist", @@ -398,9 +398,9 @@ class InterfaceReadOnly: "readonly_relpath_backup", ] - xml_tree: MscoreXmlTree + xml_tree: MuseScoreFile - def __init__(self, tree: MscoreXmlTree): + def __init__(self, tree: MuseScoreFile): self.xml_tree = tree @property @@ -433,9 +433,9 @@ def readonly_relpath_backup(self) -> str: class Interface: - xml_tree: MscoreXmlTree + xml_tree: MuseScoreFile - def __init__(self, tree: MscoreXmlTree): + def __init__(self, tree: MuseScoreFile): self.xml_tree = tree self.read_only = InterfaceReadOnly(tree) self.read_write = InterfaceReadWrite(tree.xml_root) @@ -463,7 +463,7 @@ def __setattr__(self, field: str, value): raise ReadOnlyFieldError(field) -class Meta(MscoreXmlTree): +class Meta(MuseScoreFile): def __init__(self, relpath: str): super(Meta, self).__init__(relpath) diff --git a/mscxyz/score_file_classes.py b/mscxyz/score_file_classes.py index f15a4f8..cdc15fc 100644 --- a/mscxyz/score_file_classes.py +++ b/mscxyz/score_file_classes.py @@ -1,13 +1,10 @@ """A collection of classes intended to represent one MuseScore file. -The classes build on each other hierarchically. The class hierarchy: - .. code :: - MscoreFile - MscoreXmlTree - MscoreStyleInterface - MscoreLyricsInterface + MuseScoreFile + MscoreStyleInterface + MscoreLyricsInterface """ from __future__ import annotations @@ -74,92 +71,6 @@ def list_zero_alphabet() -> List[str]: return score_dirs -############################################################################### -# Class hierarchy level 1 -############################################################################### - - -class MscoreFile: - """This class holds basic file properties of the MuseScore score file. - - :param relpath: The relative (or absolute) path of a MuseScore - file. - """ - - errors: List[Exception] - """A list to store errors.""" - - path: Path - """The absolute path of the input file.""" - - loadpath: str - """The path of the uncompressed MuseScore file in XML format file. - This path may be located in the temporary directory.""" - - relpath: str - """The relative path of the score file, for example: - ``files/by_version/2/simple.mscx``. - """ - - abspath: str - """The absolute path of the score file, for example: - ``/home/jf/test/files/by_version/2/simple.mscx``.""" - - relpath_backup: str - - dirname: str - """The name of the containing directory of the MuseScore file, for - example: ``files/by_version/2``.""" - - basename: str - """The basename of the score file, for example: ``simple``.""" - - zip_container: Optional[ZipContainer] - - def __init__(self, relpath: str) -> None: - self.errors = [] - self.relpath = relpath - self.path = Path(relpath).resolve() - self.abspath = os.path.abspath(relpath) - self.relpath_backup = relpath.replace( - "." + self.extension, "_bak." + self.extension - ) - self.dirname = os.path.dirname(relpath) - self.basename = self.filename.replace(".mscx", "") - - if self.extension == "mscz": - self.zip_container = ZipContainer(self.path) - self.loadpath = str(self.zip_container.mscx_file) - else: - self.loadpath = self.abspath - - @property - def filename(self) -> str: - """The filename of the MuseScore file, for example: - ``simple.mscx``.""" - return self.path.name - - @property - def extension(self) -> str: - """The extension (``mscx`` or ``mscz``) of the score file, for - example: ``mscx``.""" - return self.filename.split(".")[-1].lower() - - def backup(self) -> None: - """Make a copy of the MuseScore file.""" - shutil.copy2(self.relpath, self.relpath_backup) - - def export(self, extension: str = "pdf") -> None: - """Export the score to the specifed file type. - - :param extension: The extension (default: pdf) - """ - score: str = self.relpath - mscore( - ["--export-to", score.replace("." + self.extension, "." + extension), score] - ) - - class ZipContainer: """Container for the file paths of the different files in an unzipped MuseScore file @@ -236,17 +147,41 @@ def save(self, dest: str | Path) -> None: zip.close() -############################################################################### -# Class hierarchy level 2 -############################################################################### +class MuseScoreFile: + """This class holds basic file properties of the MuseScore score file. + + :param relpath: The relative (or absolute) path of a MuseScore + file. + """ + errors: List[Exception] + """A list to store errors.""" + + path: Path + """The absolute path of the input file.""" -class MscoreXmlTree(MscoreFile): - """XML tree manipulation + loadpath: str + """The path of the uncompressed MuseScore file in XML format file. + This path may be located in the temporary directory.""" - :param relpath: The relative (or absolute) path of a MuseScore file. + relpath: str + """The relative path of the score file, for example: + ``files/by_version/2/simple.mscx``. """ + abspath: str + """The absolute path of the score file, for example: + ``/home/jf/test/files/by_version/2/simple.mscx``.""" + + relpath_backup: str + + dirname: str + """The name of the containing directory of the MuseScore file, for + example: ``files/by_version/2``.""" + + basename: str + """The basename of the score file, for example: ``simple``.""" + xml_tree: _ElementTree version_major: int @@ -255,8 +190,25 @@ class MscoreXmlTree(MscoreFile): version: float """The MuseScore version, for example 2.03 or 3.01""" + zip_container: Optional[ZipContainer] + def __init__(self, relpath: str) -> None: - super(MscoreXmlTree, self).__init__(relpath) + self.errors = [] + self.relpath = relpath + self.path = Path(relpath).resolve() + self.abspath = os.path.abspath(relpath) + self.relpath_backup = relpath.replace( + "." + self.extension, "_bak." + self.extension + ) + self.dirname = os.path.dirname(relpath) + self.basename = self.filename.replace(".mscx", "") + + if self.extension == "mscz": + self.zip_container = ZipContainer(self.path) + self.loadpath = str(self.zip_container.mscx_file) + else: + self.loadpath = self.abspath + try: self.xml_tree = lxml.etree.parse(self.loadpath) except lxml.etree.XMLSyntaxError as e: @@ -266,6 +218,32 @@ def __init__(self, relpath: str) -> None: self.version = self.get_version() self.version_major = int(self.version) + @property + def filename(self) -> str: + """The filename of the MuseScore file, for example: + ``simple.mscx``.""" + return self.path.name + + @property + def extension(self) -> str: + """The extension (``mscx`` or ``mscz``) of the score file, for + example: ``mscx``.""" + return self.filename.split(".")[-1].lower() + + def backup(self) -> None: + """Make a copy of the MuseScore file.""" + shutil.copy2(self.relpath, self.relpath_backup) + + def export(self, extension: str = "pdf") -> None: + """Export the score to the specifed file type. + + :param extension: The extension (default: pdf) + """ + score: str = self.relpath + mscore( + ["--export-to", score.replace("." + self.extension, "." + extension), score] + ) + def get_version(self) -> float: """ Get the version number of the MuseScore file. @@ -434,7 +412,7 @@ def save(self, new_name: str = "", mscore: bool = False): ############################################################################### -class MscoreStyleInterface(MscoreXmlTree): +class MscoreStyleInterface(MuseScoreFile): """ Interface specialized for the style manipulation. diff --git a/tests/helper.py b/tests/helper.py index e9f7bc1..e9d8d30 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,4 +1,4 @@ -"""MscoreFile for various tests""" +"""MuseScoreFile for various tests""" from __future__ import annotations @@ -10,7 +10,7 @@ from lxml.etree import _ElementTree -from mscxyz import MscoreXmlTree +from mscxyz import MuseScoreFile # if typing.TYPE_CHECKING: @@ -66,7 +66,7 @@ def get_xml_tree(filename: str, version: int = 2) -> _ElementTree: :param version: The version of the file (default is 2). :return: The XML tree. """ - return MscoreXmlTree(get_file(filename, version)).xml_tree + return MuseScoreFile(get_file(filename, version)).xml_tree def read_file(filename: str) -> str: diff --git a/tests/test_api.py b/tests/test_api.py index 4eb96fb..2c5b3d9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -3,19 +3,18 @@ from mscxyz import ( - MscoreFile, MscoreLyricsInterface, MscoreMetaInterface, MscoreStyleInterface, - MscoreXmlTree, + MuseScoreFile, exec_mscore_binary, ) class TestApi: def test_api(self): - assert MscoreFile - assert MscoreXmlTree + assert MuseScoreFile + assert MuseScoreFile assert MscoreLyricsInterface assert MscoreMetaInterface assert MscoreStyleInterface diff --git a/tests/test_meta.py b/tests/test_meta.py index 3ba7438..a7880da 100644 --- a/tests/test_meta.py +++ b/tests/test_meta.py @@ -23,7 +23,7 @@ export_to_dict, to_underscore, ) -from mscxyz.score_file_classes import MscoreXmlTree +from mscxyz.score_file_classes import MuseScoreFile from tests import helper from tests.helper import ini_file @@ -104,7 +104,7 @@ def setup_method(self) -> None: def _init_class(self, filename: str, version: int = 2): tmp = helper.get_file(filename, version) - tree = MscoreXmlTree(tmp) + tree = MuseScoreFile(tmp) interface = InterfaceReadWrite(tree.xml_root) return interface, tree, tmp @@ -158,7 +158,7 @@ def _test_set_all_values(self, version: int): assert getattr(interface, field) == field + "_test" tree.save() - tree = MscoreXmlTree(tmp) + tree = MuseScoreFile(tmp) interface = InterfaceReadWrite(tree.xml_root) assert interface.combined_composer == "vbox_composer_test" @@ -228,7 +228,7 @@ def setup_method(self): "readonly_relpath_backup", ) self.tmp = helper.get_file("simple.mscx") - self.xml_tree = MscoreXmlTree(self.tmp) + self.xml_tree = MuseScoreFile(self.tmp) self.interface = InterfaceReadOnly(self.xml_tree) def test_exception(self): @@ -290,7 +290,7 @@ def setup_method(self): ] self.tmp = helper.get_file("meta-all-values.mscx") - self.xml_tree = MscoreXmlTree(self.tmp) + self.xml_tree = MuseScoreFile(self.tmp) self.interface = Interface(self.xml_tree) def test_static_method_get_all_fields(self): @@ -312,7 +312,7 @@ def test_exception(self): class TestClassMetaTag: def _init_class(self, filename: str, version: int = 2): tmp = helper.get_file(filename, version) - tree = MscoreXmlTree(tmp) + tree = MuseScoreFile(tmp) meta = MetaTag(tree.xml_root) return meta, tree, tmp @@ -342,7 +342,7 @@ def test_set2(self) -> None: meta.workTitle = "WT" meta.movement_title = "MT" tree.save() - tree = MscoreXmlTree(tmp) + tree = MuseScoreFile(tmp) meta = MetaTag(tree.xml_root) assert meta.work_title == "WT" assert meta.movementTitle == "MT" @@ -354,7 +354,7 @@ def test_set3(self) -> None: meta.workTitle = "WT" meta.movement_title = "MT" tree.save() - tree = MscoreXmlTree(tmp) + tree = MuseScoreFile(tmp) meta = MetaTag(tree.xml_root) assert meta.work_title == "WT" assert meta.movementTitle == "MT" @@ -382,9 +382,9 @@ def test_clean(self) -> None: class TestClassVbox: def _init_class( self, filename: str, version: int = 2 - ) -> tuple[Vbox, MscoreXmlTree, str]: + ) -> tuple[Vbox, MuseScoreFile, str]: tmp = helper.get_file(filename, version) - tree = MscoreXmlTree(tmp) + tree = MuseScoreFile(tmp) vbox = Vbox(tree.xml_root) return vbox, tree, tmp @@ -421,12 +421,12 @@ def test_get_exception(self) -> None: def _assert_set(self, filename: str, version: int = 2) -> None: tmp = helper.get_file(filename, version) - tree = MscoreXmlTree(tmp) + tree = MuseScoreFile(tmp) vbox = Vbox(tree.xml_root) vbox.Title = "lol" vbox.composer = "lol" tree.save() - tree = MscoreXmlTree(tmp) + tree = MuseScoreFile(tmp) vbox = Vbox(tree.xml_root) assert vbox.title == "lol" assert vbox.Composer == "lol" @@ -452,9 +452,9 @@ def test_set_exception(self) -> None: class TestClassCombined: - def _init_class(self, filename: str) -> tuple[Combined, MscoreXmlTree, str]: + def _init_class(self, filename: str) -> tuple[Combined, MuseScoreFile, str]: tmp = helper.get_file(filename) - tree = MscoreXmlTree(tmp) + tree = MuseScoreFile(tmp) combined = Combined(tree.xml_root) return combined, tree, tmp diff --git a/tests/test_score_file_classes.py b/tests/test_score_file_classes.py index adad8bd..40e16e9 100644 --- a/tests/test_score_file_classes.py +++ b/tests/test_score_file_classes.py @@ -15,9 +15,8 @@ import mscxyz from mscxyz.score_file_classes import ( - MscoreFile, MscoreStyleInterface, - MscoreXmlTree, + MuseScoreFile, ZipContainer, list_scores, list_zero_alphabet, @@ -88,9 +87,9 @@ def test_function_list_zero_alphabet(self): assert result[26] == "z" -class TestMscoreFile: +class TestClassMuseScoreFile: def setup_method(self) -> None: - self.file = MscoreFile(helper.get_file("simple.mscx")) + self.file = MuseScoreFile(helper.get_file("simple.mscx")) def test_attribute_path(self) -> None: assert self.file.relpath @@ -114,11 +113,76 @@ def test_attribute_extension(self) -> None: def test_attribute_basename(self) -> None: assert self.file.basename == "simple" + def test_method_merge_style(self) -> None: + tree = MuseScoreFile(helper.get_file("simple.mscx")) + styles = """ + + center + bottom + 0 + -1 + spatium + Form Section + Alegreya Sans + 12 + 1 + 1 + 1 + 0.1 + 0.2 + 0 + + + """ + tree.clean() + tree.merge_style(styles) + + xml_tree = tree.xml_tree + result = xml_tree.xpath("/museScore/Score/Style") + assert result[0][0][0].tag == "halign" + assert result[0][0][0].text == "center" + + def test_method_clean(self) -> None: + tmp = helper.get_file("clean.mscx", version=3) + tree = MuseScoreFile(tmp) + tree.clean() + tree.save() + tree = MuseScoreFile(tmp) + xml_tree = tree.xml_tree + assert xml_tree.xpath("/museScore/Score/Style") == [] + assert xml_tree.xpath("//LayoutBreak") == [] + assert xml_tree.xpath("//StemDirection") == [] + assert xml_tree.xpath("//font") == [] + assert xml_tree.xpath("//b") == [] + assert xml_tree.xpath("//i") == [] + assert xml_tree.xpath("//pos") == [] + assert xml_tree.xpath("//offset") == [] + + def test_method_save(self) -> None: + tmp = helper.get_file("simple.mscx") + tree = MuseScoreFile(tmp) + tree.save() + result = helper.read_file(tmp) + assert '' in result + + def test_method_save_new_name(self): + tmp = helper.get_file("simple.mscx") + tree = MuseScoreFile(tmp) + tree.save(new_name=tmp) + result = helper.read_file(tmp) + assert '' in result + + def test_mscz(self): + tmp = helper.get_file("simple.mscz") + tree = MuseScoreFile(tmp) + result = tree.xml_tree.xpath("/museScore/Score/Style") + assert result[0].tag == "Style" + @pytest.mark.skip("Not implemented yet") -class TestMscoreFileMscz: +class TestMuseScoreFileMscz: def setup_method(self): - self.file = MscoreFile(helper.get_file("simple.mscz")) + self.file = MuseScoreFile(helper.get_file("simple.mscz")) def test_attribute_extension(self): assert self.file.extension == "mscz" @@ -127,9 +191,9 @@ def test_attribute_loadpath(self): assert "simple.mscx" in self.file.loadpath -class TestMscoreFileMscz4: +class TestMuseScoreFileMscz4: def setup_method(self): - self.file = MscoreFile( + self.file = MuseScoreFile( helper.get_file("test.mscz", version=4), ) @@ -176,8 +240,8 @@ def test_method_save(self) -> None: assert container.mscx_file.exists() -class TestMscoreXmlTreeVersion2: - tree = MscoreXmlTree(helper.get_file("simple.mscz", 2)) +class TestMuseScoreFileVersion2: + tree = MuseScoreFile(helper.get_file("simple.mscz", 2)) def test_property_version(self) -> None: assert self.tree.version == 2.06 @@ -189,8 +253,8 @@ def test_method_get_version(self) -> None: assert self.tree.get_version() == 2.06 -class TestMscoreXmlTreeVersion3: - tree = MscoreXmlTree(helper.get_file("simple.mscz", 3)) +class TestMuseScoreFileVersion3: + tree = MuseScoreFile(helper.get_file("simple.mscz", 3)) def test_property_version(self) -> None: assert self.tree.version == 3.01 @@ -202,8 +266,8 @@ def test_method_get_version(self) -> None: assert self.tree.get_version() == 3.01 -class TestMscoreXmlTreeVersion4: - tree = MscoreXmlTree(helper.get_file("simple.mscz", 4)) +class TestMuseScoreFileVersion4: + tree = MuseScoreFile(helper.get_file("simple.mscz", 4)) def test_property_version(self) -> None: assert self.tree.version == 4.2 @@ -215,73 +279,6 @@ def test_method_get_version(self) -> None: assert self.tree.get_version() == 4.2 -class TestClassMscoreXmlTree: - def test_method_merge_style(self) -> None: - tree = MscoreXmlTree(helper.get_file("simple.mscx")) - styles = """ - - center - bottom - 0 - -1 - spatium - Form Section - Alegreya Sans - 12 - 1 - 1 - 1 - 0.1 - 0.2 - 0 - - - """ - tree.clean() - tree.merge_style(styles) - - xml_tree = tree.xml_tree - result = xml_tree.xpath("/museScore/Score/Style") - assert result[0][0][0].tag == "halign" - assert result[0][0][0].text == "center" - - def test_method_clean(self) -> None: - tmp = helper.get_file("clean.mscx", version=3) - tree = MscoreXmlTree(tmp) - tree.clean() - tree.save() - tree = MscoreXmlTree(tmp) - xml_tree = tree.xml_tree - assert xml_tree.xpath("/museScore/Score/Style") == [] - assert xml_tree.xpath("//LayoutBreak") == [] - assert xml_tree.xpath("//StemDirection") == [] - assert xml_tree.xpath("//font") == [] - assert xml_tree.xpath("//b") == [] - assert xml_tree.xpath("//i") == [] - assert xml_tree.xpath("//pos") == [] - assert xml_tree.xpath("//offset") == [] - - def test_method_save(self) -> None: - tmp = helper.get_file("simple.mscx") - tree = MscoreXmlTree(tmp) - tree.save() - result = helper.read_file(tmp) - assert '' in result - - def test_method_save_new_name(self): - tmp = helper.get_file("simple.mscx") - tree = MscoreXmlTree(tmp) - tree.save(new_name=tmp) - result = helper.read_file(tmp) - assert '' in result - - def test_mscz(self): - tmp = helper.get_file("simple.mscz") - tree = MscoreXmlTree(tmp) - result = tree.xml_tree.xpath("/museScore/Score/Style") - assert result[0].tag == "Style" - - class TestClean: def _test_clean(self, version: int = 2) -> None: tmp: str = helper.get_file("formats.mscx", version) @@ -454,7 +451,7 @@ def assertDiff(self, filename: str, version: int = 2): saved = orig.replace(".mscx", "_saved.mscx") tmp = helper.get_file(filename, version=version) shutil.copy2(tmp, orig) - tree = MscoreXmlTree(tmp) + tree = MuseScoreFile(tmp) tree.save(new_name=saved) assert filecmp.cmp(orig, saved) os.remove(orig)