diff --git a/README.rst b/README.rst index 9b4a22c..e08f92e 100644 --- a/README.rst +++ b/README.rst @@ -672,7 +672,7 @@ CLI Usage -n, --no-whitespace Replace all whitespaces with dashes or sometimes underlines. -K FIELDS, --skip-if-empty FIELDS Skip rename action if FIELDS are empty. Separate FIELDS - using commas: combined_composer,combined_title + using commas: composer,title -t RENAME_TARGET, --target RENAME_TARGET Target directory diff --git a/docs/cli.rst b/docs/cli.rst index 17842d0..26c7864 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -407,7 +407,7 @@ Comande line interface -n, --no-whitespace Replace all whitespaces with dashes or sometimes underlines. -K FIELDS, --skip-if-empty FIELDS Skip rename action if FIELDS are empty. Separate FIELDS - using commas: combined_composer,combined_title + using commas: composer,title -t RENAME_TARGET, --target RENAME_TARGET Target directory diff --git a/mscxyz/cli.py b/mscxyz/cli.py index d041942..30e65f6 100644 --- a/mscxyz/cli.py +++ b/mscxyz/cli.py @@ -418,7 +418,7 @@ def _split_lines(self, text: typing.Text, width: int) -> typing.List[str]: "-f", "--format", dest="rename_format", - default="$combined_title ($combined_composer)", + default="$title ($composer)", help="Format string.", ) @@ -452,7 +452,7 @@ def _split_lines(self, text: typing.Text, width: int) -> typing.List[str]: dest="rename_skip", metavar="FIELDS", help="Skip rename action if FIELDS are empty. Separate FIELDS using " - "commas: combined_composer,combined_title", + "commas: composer,title", ) group_rename.add_argument( @@ -811,7 +811,7 @@ def list_styles(version: int) -> None: if args.meta_dist: for a in args.meta_dist: - score.meta.distribute_field(source_fields=a[0], format_string=a[1]) + score.fields.distribute(source_fields=a[0], format_string=a[1]) if args.meta_delete: score.meta.delete_duplicates() diff --git a/mscxyz/fields.py b/mscxyz/fields.py index 5aa1000..aa885d4 100644 --- a/mscxyz/fields.py +++ b/mscxyz/fields.py @@ -3,17 +3,20 @@ from __future__ import annotations import json +import re import typing from dataclasses import dataclass from pathlib import Path -from typing import Any, Union +from typing import Any, Mapping, Union + +from mscxyz.meta import FormatStringNoFieldError, UnmatchedFormatStringError if typing.TYPE_CHECKING: from mscxyz.score import Score FieldValue = Union[str, int, float] -FieldsExport = dict[str, FieldValue] +FieldsExport = Mapping[str, FieldValue] @dataclass @@ -226,8 +229,13 @@ def __init__(self, score: "Score") -> None: def names(self) -> tuple[str, ...]: return tuple(self.__fields_by_name.keys()) - def __access_attr(self, attr_path: str) -> Any | None: - attrs = attr_path.split(".") + def get_field(self, name: str) -> Field: + return self.__fields_by_name[name] + + def get(self, name: str) -> Any | None: + field = self.get_field(name) + + attrs = field.attr_path.split(".") value = self.score for attr in attrs: value = getattr(value, attr) @@ -236,9 +244,19 @@ def __access_attr(self, attr_path: str) -> Any | None: return value - def get(self, name: str) -> Any | None: - field = self.__fields_by_name[name] - return self.__access_attr(field.attr_path) + def set(self, name: str, value: Any) -> Any | None: + field = self.get_field(name) + attrs = field.attr_path.split(".") + + last = attrs.pop() + + obj = self.score + for attr in attrs: + obj = getattr(obj, attr) + if obj is None: + raise Exception(f"Cannot set attribute {field.attr_path}") + + setattr(obj, last, value) def show(self, pre: dict[str, str], post: dict[str, str]) -> None: pass @@ -283,7 +301,7 @@ def show(self, pre: dict[str, str], post: dict[str, str]) -> None: # print("{}: {}".format(utils.color(field, field_color), " ".join(line))) - def export_to_dict(self) -> FieldsExport: + def export_to_dict(self) -> dict[str, FieldValue]: output: FieldsExport = {} for field in self.names: value = self.get(field) @@ -293,6 +311,26 @@ def export_to_dict(self) -> FieldsExport: output[field] = value return output + def distribute(self, source_fields: str, format_string: str) -> None: + f: list[str] = source_fields.split(",") + for source_field in f: + source = self.get(source_field) + source = str(source) + + fields = re.findall(r"\$([a-z_]*)", format_string) + if not fields: + raise FormatStringNoFieldError(format_string) + regex = re.sub(r"\$[a-z_]*", "(.*)", format_string) + match = re.search(regex, source) + if not match: + raise UnmatchedFormatStringError(format_string, source) + values = match.groups() + results: dict[str, str] = dict(zip(fields, values)) + if results: + for field, value in results.items(): + self.set(field, value) + return + def export_json(self) -> Path: """ Export the data as a JSON file. diff --git a/mscxyz/meta.py b/mscxyz/meta.py index 3005a2e..1a8c58d 100644 --- a/mscxyz/meta.py +++ b/mscxyz/meta.py @@ -47,27 +47,6 @@ def __init__(self, format_string: str) -> None: Exception.__init__(self, self.msg) -def distribute_field(source: str, format_string: str) -> dict[str, str]: - """ - Distributes the values from the source string into a dictionary based on the format string. - - :param source: The source string from which values will be extracted. - :param format_string: The format string that specifies the pattern of the values to be extracted. - :return: A dictionary mapping field names to their corresponding values. - :raises FormatStringNoFieldError: If the format string does not contain any field markers. - :raises UnmatchedFormatStringError: If the format string does not match the source string. - """ - fields = re.findall(r"\$([a-z_]*)", format_string) - if not fields: - raise FormatStringNoFieldError(format_string) - regex = re.sub(r"\$[a-z_]*", "(.*)", format_string) - match = re.search(regex, source) - if not match: - raise UnmatchedFormatStringError(format_string, source) - values = match.groups() - return dict(zip(fields, values)) - - def to_underscore(field: str) -> str: """ Convert a camel case string to snake case. @@ -890,16 +869,6 @@ def sync_fields(self) -> None: self.combined.composer = self.combined.composer self.combined.lyricist = self.combined.lyricist - def distribute_field(self, source_fields: str, format_string: str) -> None: - f: list[str] = source_fields.split(",") - for source_field in f: - source = getattr(self.interface, source_field) - results: dict[str, str] = distribute_field(source, format_string) - if results: - for field, value in results.items(): - setattr(self.interface, field, value) - return - def write_to_log_file(self, log_file: str, format_string: str) -> None: log = open(log_file, "w") log.write(tmep.parse(format_string, self.interface.export_to_dict()) + "\n") diff --git a/mscxyz/rename.py b/mscxyz/rename.py index bb2daef..29ef8ae 100644 --- a/mscxyz/rename.py +++ b/mscxyz/rename.py @@ -10,6 +10,7 @@ import tmep from tmep.format import alphanum, asciify, nowhitespace +from mscxyz.fields import FieldsExport from mscxyz.score import Score from mscxyz.settings import get_args from mscxyz.utils import color @@ -23,11 +24,12 @@ def create_dir(path: str) -> None: raise -def prepare_fields(fields: dict[str, str]) -> dict[str, str]: +def prepare_fields(fields: FieldsExport) -> dict[str, str]: args = get_args() - out: dict[str, str] = {} + output: dict[str, str] = {} for field, value in fields.items(): if value: + value = str(value) if args.rename_alphanum: value = alphanum(value) value = value.strip() @@ -37,11 +39,11 @@ def prepare_fields(fields: dict[str, str]) -> dict[str, str]: value = nowhitespace(value) value = value.strip() value = value.replace("/", "-") - out[field] = value - return out + output[field] = value + return output -def apply_format_string(fields: dict[str, str]) -> str: +def apply_format_string(fields: FieldsExport) -> str: args = get_args() fields = prepare_fields(fields) name = tmep.parse(args.rename_format, fields) @@ -66,13 +68,13 @@ def get_checksum(filename: str) -> str: def rename(score: Score) -> Score: args = get_args() - meta_values: dict[str, str] = score.meta.interface.export_to_dict() + meta_values = score.fields.export_to_dict() target_filename: str = apply_format_string(meta_values) if args.rename_skip: skips: list[str] = args.rename_skip.split(",") for skip in skips: - if not meta_values[skip]: + if skip not in meta_values: print(color("Field “{}” is empty! Skipping".format(skip), "red")) return score diff --git a/mscxyz/settings.py b/mscxyz/settings.py index 398be60..392e9b4 100644 --- a/mscxyz/settings.py +++ b/mscxyz/settings.py @@ -55,7 +55,7 @@ class DefaultArguments: rename_rename: bool = False rename_alphanum: bool = False rename_ascii: bool = False - rename_format: str = "$combined_title ($combined_composer)" + rename_format: str = "$title ($composer)" rename_no_whitespace = False rename_skip: Optional[str] = None rename_target: Optional[str] = None diff --git a/tests/test_cli.py b/tests/test_cli.py index 4d94e46..08920f0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -45,7 +45,7 @@ def test_args_general_rename(self) -> None: assert args.rename_rename assert args.rename_alphanum is False assert args.rename_ascii is False - assert args.rename_format == "$combined_title ($combined_composer)" + assert args.rename_format == "$title ($composer)" assert args.rename_target is None diff --git a/tests/test_fields.py b/tests/test_fields.py index 880d36f..6ab5fb2 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -78,3 +78,14 @@ def test_method_export_to_dict(self, fields: FieldsManager) -> None: def test_method_get(self, fields: FieldsManager) -> None: assert fields.get("title") == "Title" + + def test_method_set(self, fields: FieldsManager) -> None: + new = "New Title" + fields.set("title", new) + assert fields.get("title") == new + + def test_distribute(self, fields: FieldsManager) -> None: + assert fields.distribute("title,compose", "$title - $composer") == { + "composer": "Queen", + "title": "We are the champions", + } diff --git a/tests/test_how_to.py b/tests/test_how_to.py index 6133f55..b3519db 100644 --- a/tests/test_how_to.py +++ b/tests/test_how_to.py @@ -243,7 +243,7 @@ def test_rename(self, tmp_path: Path) -> None: "--target", dest, "--format", - "%lower{%shorten{$combined_title,1}}/$combined_title", + "%lower{%shorten{$title,1}}/$title", "--no-whitespace", src, ).execute() diff --git a/tests/test_meta.py b/tests/test_meta.py index e5d7f9f..433a821 100644 --- a/tests/test_meta.py +++ b/tests/test_meta.py @@ -17,7 +17,6 @@ Meta, Metatag, Vbox, - distribute_field, export_to_dict, to_underscore, ) @@ -60,11 +59,6 @@ def test_format_string_no_field_error(self) -> None: class TestFunctions: - def test_distribute_field(self) -> None: - assert distribute_field( - "We are the champions - Queen", "$title - $composer" - ) == {"composer": "Queen", "title": "We are the champions"} - def test_to_underscore(self) -> None: assert to_underscore("PascalCase") == "_pascal_case" assert to_underscore("lowerCamelCase") == "lower_camel_case" @@ -402,23 +396,24 @@ def test_distribute_field(self) -> None: Cli( "--distribute-field", "vbox_title", - "$combined_title - $combined_composer", + "$title - $composer", ) .append_score("meta-distribute-field.mscz") .execute() ) - i = c.post.meta.interface - assert i.vbox_composer == "Composer" - assert i.metatag_composer == "Composer" - assert i.vbox_title == "Title" - assert i.metatag_work_title == "Title" + f = c.post.fields + + assert f.get("vbox_composer") == "Composer" + assert f.get("metatag_composer") == "Composer" + assert f.get("vbox_title") == "Title" + assert f.get("metatag_work_title") == "Title" def test_distribute_field_multple_source_fields(self) -> None: c = ( Cli( "--distribute-field", "vbox_title,readonly_basename", - "$combined_title - $combined_composer", + "$title - $composer", ) .append_score("Title - Composer.mscz") .execute() @@ -766,7 +761,7 @@ def test_with_templating(self) -> None: def test_option_log(tmp_path: Path) -> None: log = tmp_path / "log.txt" - Cli("--log", log, "$combined_title-$combined_composer").execute() + Cli("--log", log, "$title-$composer").execute() assert open(log, "r").readline() == "Title-Composer\n" diff --git a/tests/test_rename.py b/tests/test_rename.py index 18b6b82..fbf4616 100644 --- a/tests/test_rename.py +++ b/tests/test_rename.py @@ -14,7 +14,7 @@ class TestFunctions: def test_function_prepare_fields(self) -> None: reset_args() - fields: dict[str, str] = { + fields = { "field1": " Subtitle ", "field2": "Title / Composer", } @@ -27,7 +27,7 @@ def test_function_prepare_fields(self) -> None: def test_function_apply_format_string(self) -> None: reset_args() score = get_score("meta-all-values.mscx") - fields: dict[str, str] = score.meta.interface.export_to_dict() + fields = score.fields.export_to_dict() name: str = rename.apply_format_string(fields) assert name == "vbox_title (vbox_composer)" @@ -36,7 +36,7 @@ def test_function_get_checksum(self) -> None: assert rename.get_checksum(tmp) == "dacd912aa0f6a1a67c3b13bb947395509e19dce2" -class TestIntegration: +class TestCli: @pytest.mark.parametrize("version", supported_versions) def test_simple(self, version: int, cwd_tmpdir: Path) -> None: stdout: str = Cli(