diff --git a/picard/album.py b/picard/album.py index 95a9c3f38..cbaf7febb 100644 --- a/picard/album.py +++ b/picard/album.py @@ -82,7 +82,7 @@ from picard.plugin import ( from picard.script import ( ScriptError, ScriptParser, - enabled_tagger_scripts_texts, + iter_active_tagging_scripts, ) from picard.track import Track from picard.util import ( @@ -474,21 +474,21 @@ class Album(DataObject, MetadataItem): track.metadata_images_changed.connect(self.update_metadata_images) # Prepare parser for user's script - for s_name, s_text in enabled_tagger_scripts_texts(): + for script in iter_active_tagging_scripts(): parser = ScriptParser() for track in self._new_tracks: # Run tagger script for each track try: - parser.eval(s_text, track.metadata) + parser.eval(script.content, track.metadata) except ScriptError: - log.exception("Failed to run tagger script %s on track", s_name) + log.exception("Failed to run tagger script %s on track", script.name) track.metadata.strip_whitespace() track.scripted_metadata.update(track.metadata) # Run tagger script for the album itself try: - parser.eval(s_text, self._new_metadata) + parser.eval(script.content, self._new_metadata) except ScriptError: - log.exception("Failed to run tagger script %s on album", s_name) + log.exception("Failed to run tagger script %s on album", script.name) self._new_metadata.strip_whitespace() unmatched_files = [file for track in self.tracks for file in track.files] diff --git a/picard/config_upgrade.py b/picard/config_upgrade.py index 0b34f0743..ee0b7685c 100644 --- a/picard/config_upgrade.py +++ b/picard/config_upgrade.py @@ -429,19 +429,19 @@ def upgrade_to_v2_7_0dev3(config): """ from picard.script import get_file_naming_script_presets from picard.script.serializer import ( - FileNamingScript, - ScriptImportError, + FileNamingScriptInfo, + ScriptSerializerFromFileError, ) scripts = {} for item in config.setting.raw_value('file_naming_scripts') or []: try: - script_item = FileNamingScript().create_from_yaml(item, create_new_id=False) + script_item = FileNamingScriptInfo().create_from_yaml(item, create_new_id=False) scripts[script_item['id']] = script_item.to_dict() - except ScriptImportError: + except ScriptSerializerFromFileError: log.error("Error converting file naming script") script_list = set(scripts.keys()) | set(map(lambda item: item['id'], get_file_naming_script_presets())) if config.setting['selected_file_naming_script_id'] not in script_list: - script_item = FileNamingScript( + script_item = FileNamingScriptInfo( script=config.setting.value('file_naming_format', TextOption), title=_("Primary file naming script"), readonly=False, diff --git a/picard/script/__init__.py b/picard/script/__init__.py index 8d9ec9ccf..594eb6445 100644 --- a/picard/script/__init__.py +++ b/picard/script/__init__.py @@ -63,7 +63,43 @@ from picard.script.parser import ( # noqa: F401 # pylint: disable=unused-import ScriptUnknownFunction, ScriptVariable, ) -from picard.script.serializer import FileNamingScript +from picard.script.serializer import FileNamingScriptInfo + + +class TaggingScriptSetting: + def __init__(self, pos=0, name="", enabled=False, content=""): + self.pos = pos + self.name = name + self.enabled = enabled + self.content = content + + +def iter_tagging_scripts_from_config(config=None): + if config is None: + config = get_config() + yield from iter_tagging_scripts_from_tuples(config.setting['list_of_scripts']) + + +def iter_tagging_scripts_from_tuples(tuples): + for pos, name, enabled, content in tuples: + yield TaggingScriptSetting(pos=pos, name=name, enabled=enabled, content=content) + + +def save_tagging_scripts_to_config(scripts, config=None): + if config is None: + config = get_config() + config.setting['list_of_scripts'] = [(s.pos, s.name, s.enabled, s.content) for s in scripts] + + +def iter_active_tagging_scripts(config=None): + """Returns an iterator over the enabled and not empty tagging scripts.""" + if config is None: + config = get_config() + if not config.setting['enable_tagger_scripts']: + return + for script in iter_tagging_scripts_from_config(config=config): + if script.enabled and script.content: + yield script class ScriptFunctionDocError(Exception): @@ -111,15 +147,6 @@ def script_function_documentation_all(fmt='markdown', pre='', return "\n".join(doc_elements) -def enabled_tagger_scripts_texts(): - """Returns an iterator over the enabled tagger scripts. - For each script, you'll get a tuple consisting of the script name and text""" - config = get_config() - if not config.setting['enable_tagger_scripts']: - return [] - return [(s_name, s_text) for _s_pos, s_name, s_enabled, s_text in config.setting['list_of_scripts'] if s_enabled and s_text] - - def get_file_naming_script(settings): """Retrieve the file naming script. @@ -146,7 +173,7 @@ def get_file_naming_script_presets(): """Generator of preset example file naming script objects. Yields: - FileNamingScript: the next example FileNamingScript object + FileNamingScriptInfo: the next example FileNamingScriptInfo object """ AUTHOR = "MusicBrainz Picard Development Team" DESCRIPTION = _("This preset example file naming script does not require any special settings, tagging scripts or plugins.") @@ -158,7 +185,7 @@ def get_file_naming_script_presets(): 'title': _(title), } - yield FileNamingScript( + yield FileNamingScriptInfo( id=DEFAULT_NAMING_PRESET_ID, title=preset_title(1, N_("Default file naming script")), script=DEFAULT_FILE_NAMING_FORMAT, @@ -170,7 +197,7 @@ def get_file_naming_script_presets(): script_language_version="1.0", ) - yield FileNamingScript( + yield FileNamingScriptInfo( id="Preset 2", title=preset_title(2, N_("[album artist]/[album]/[track #]. [title]")), script="%albumartist%/\n" @@ -184,7 +211,7 @@ def get_file_naming_script_presets(): script_language_version="1.0", ) - yield FileNamingScript( + yield FileNamingScriptInfo( id="Preset 3", title=preset_title(3, N_("[album artist]/[album]/[disc and track #] [artist] - [title]")), script="$if2(%albumartist%,%artist%)/\n" diff --git a/picard/script/serializer.py b/picard/script/serializer.py index 65763bfb9..cc5a53dd0 100644 --- a/picard/script/serializer.py +++ b/picard/script/serializer.py @@ -49,7 +49,7 @@ from picard.util import make_filename_from_title @unique -class PicardScriptType(IntEnum): +class ScriptSerializerType(IntEnum): """Picard Script object types """ BASE = 0 @@ -57,16 +57,28 @@ class PicardScriptType(IntEnum): FILENAMING = 2 -class ScriptImportExportError(Exception): +class ScriptSerializerError(Exception): + """Base exception class for ScriptSerializer errors""" + + +class ScriptSerializerImportExportError(ScriptSerializerError): def __init__(self, *args, format=None, filename=None, error_msg=None): + super().__init__(*args) self.format = format self.filename = filename self.error_msg = error_msg -class ScriptImportError(Exception): - def __init__(self, *args): - super().__init__(*args) +class ScriptSerializerImportError(ScriptSerializerImportExportError): + """Exception raised during script import""" + + +class ScriptSerializerExportError(ScriptSerializerImportExportError): + """Exception raised during script export""" + + +class ScriptSerializerFromFileError(ScriptSerializerError): + """Exception raised when converting a file to a ScriptSerializer""" class MultilineLiteral(str): @@ -80,12 +92,12 @@ class MultilineLiteral(str): yaml.add_representer(MultilineLiteral, MultilineLiteral.yaml_presenter) -class PicardScript(): +class ScriptSerializer(): """Base class for Picard script objects. """ # Base class developed to support future tagging script class as possible replacement for currently used tuples in config.setting["list_of_scripts"]. - TYPE = PicardScriptType.BASE + TYPE = ScriptSerializerType.BASE OUTPUT_FIELDS = ('title', 'script_language_version', 'script', 'id') # Don't automatically trigger changing the `script_last_updated` property when updating these properties. @@ -226,7 +238,7 @@ class PicardScript(): with open(filename, 'w', encoding='utf-8') as o_file: o_file.write(script_text) except OSError as error: - raise ScriptImportExportError(format=FILE_ERROR_EXPORT, filename=filename, error_msg=error.strerror) + raise ScriptSerializerExportError(format=FILE_ERROR_EXPORT, filename=filename, error_msg=error.strerror) dialog = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Icon.Information, _("Export Script"), @@ -255,14 +267,14 @@ class PicardScript(): with open(filename, 'r', encoding='utf-8') as i_file: file_content = i_file.read() except OSError as error: - raise ScriptImportExportError(format=FILE_ERROR_IMPORT, filename=filename, error_msg=error.strerror) + raise ScriptSerializerImportError(format=FILE_ERROR_IMPORT, filename=filename, error_msg=error.strerror) if not file_content.strip(): - raise ScriptImportExportError(format=FILE_ERROR_IMPORT, filename=filename, error_msg=N_("The file was empty")) + raise ScriptSerializerImportError(format=FILE_ERROR_IMPORT, filename=filename, error_msg=N_("The file was empty")) if file_type == cls._file_types()['package']: try: return cls().create_from_yaml(file_content) - except ScriptImportError as error: - raise ScriptImportExportError(format=FILE_ERROR_DECODE, filename=filename, error_msg=error) + except ScriptSerializerFromFileError as error: + raise ScriptSerializerImportError(format=FILE_ERROR_DECODE, filename=filename, error_msg=error) else: return cls( title=_("Imported from %s") % filename, @@ -283,9 +295,9 @@ class PicardScript(): """ new_object = cls() if not isinstance(script_dict, Mapping): - raise ScriptImportError(N_("Argument is not a dictionary")) + raise ScriptSerializerFromFileError(N_("Argument is not a dictionary")) if 'title' not in script_dict or 'script' not in script_dict: - raise ScriptImportError(N_("Invalid script package")) + raise ScriptSerializerFromFileError(N_("Invalid script package")) new_object.update_from_dict(script_dict) if create_new_id or not new_object['id']: new_object._set_new_id() @@ -327,9 +339,9 @@ class PicardScript(): new_object = cls() yaml_dict = yaml.safe_load(yaml_string) if not isinstance(yaml_dict, dict): - raise ScriptImportError(N_("File content not a dictionary")) + raise ScriptSerializerFromFileError(N_("File content not a dictionary")) if 'title' not in yaml_dict or 'script' not in yaml_dict: - raise ScriptImportError(N_("Invalid script package")) + raise ScriptSerializerFromFileError(N_("Invalid script package")) new_object.update_from_dict(yaml_dict) if create_new_id or not new_object['id']: new_object._set_new_id() @@ -367,10 +379,10 @@ class PicardScript(): )) -class TaggingScript(PicardScript): +class TaggingScriptInfo(ScriptSerializer): """Picard tagging script class """ - TYPE = PicardScriptType.TAGGER + TYPE = ScriptSerializerType.TAGGER OUTPUT_FIELDS = ('title', 'script_language_version', 'script', 'id') def __init__(self, script='', title='', id=None, last_updated=None, script_language_version=None): @@ -385,10 +397,10 @@ class TaggingScript(PicardScript): super().__init__(script=script, title=title, id=id, last_updated=last_updated, script_language_version=script_language_version) -class FileNamingScript(PicardScript): +class FileNamingScriptInfo(ScriptSerializer): """Picard file naming script class """ - TYPE = PicardScriptType.FILENAMING + TYPE = ScriptSerializerType.FILENAMING OUTPUT_FIELDS = ('title', 'description', 'author', 'license', 'version', 'last_updated', 'script_language_version', 'script', 'id') def __init__( diff --git a/picard/track.py b/picard/track.py index d3c35cd77..7d42fd3ff 100644 --- a/picard/track.py +++ b/picard/track.py @@ -71,7 +71,7 @@ from picard.metadata import ( from picard.script import ( ScriptError, ScriptParser, - enabled_tagger_scripts_texts, + iter_active_tagging_scripts, ) from picard.util import pattern_as_regex from picard.util.imagelist import ImageList @@ -201,12 +201,12 @@ class Track(DataObject, FileListItem): @staticmethod def run_scripts(metadata, strip_whitespace=False): - for s_name, s_text in enabled_tagger_scripts_texts(): + for script in iter_active_tagging_scripts(): parser = ScriptParser() try: - parser.eval(s_text, metadata) + parser.eval(script.content, metadata) except ScriptError: - log.exception("Failed to run tagger script %s on track", s_name) + log.exception("Failed to run tagger script %s on track", script.name) if strip_whitespace: metadata.strip_whitespace() diff --git a/picard/ui/itemviews/basetreeview.py b/picard/ui/itemviews/basetreeview.py index d223d5cbc..82003dab5 100644 --- a/picard/ui/itemviews/basetreeview.py +++ b/picard/ui/itemviews/basetreeview.py @@ -75,6 +75,7 @@ from picard.extension_points.item_actions import ( ) from picard.file import File from picard.i18n import gettext as _ +from picard.script import iter_tagging_scripts_from_tuples from picard.track import ( NonAlbumTrack, Track, @@ -403,8 +404,6 @@ class BaseTreeView(QtWidgets.QTreeWidget): CollectionMenu(selected_albums, _("Collections"), menu), ) - scripts = config.setting['list_of_scripts'] - if plugin_actions: plugin_menu = QtWidgets.QMenu(_("P&lugins"), menu) plugin_menu.setIcon(self.icon_plugins) @@ -424,8 +423,9 @@ class BaseTreeView(QtWidgets.QTreeWidget): action_menu = plugin_menus[key] = action_menu.addMenu(key[-1]) action_menu.addAction(action) + scripts = config.setting['list_of_scripts'] if scripts: - scripts_menu = ScriptsMenu(scripts, _("&Run scripts"), menu) + scripts_menu = ScriptsMenu(iter_tagging_scripts_from_tuples(scripts), _("&Run scripts"), menu) scripts_menu.setIcon(self.icon_plugins) add_actions( '-', diff --git a/picard/ui/options/profiles.py b/picard/ui/options/profiles.py index 221349e46..07dba374f 100644 --- a/picard/ui/options/profiles.py +++ b/picard/ui/options/profiles.py @@ -44,7 +44,10 @@ from picard.i18n import ( gettext_constants, ) from picard.profile import profile_groups_values -from picard.script import get_file_naming_script_presets +from picard.script import ( + get_file_naming_script_presets, + iter_tagging_scripts_from_tuples, +) from picard.util import get_base_title from picard.ui.forms.ui_options_profiles import Ui_ProfileEditorDialog @@ -249,7 +252,7 @@ class ProfilesOptionsPage(OptionsPage): return _("Unknown script") def _get_scripts_list(self, scripts): - enabled_scripts = ['
  • %s
  • ' % name for (pos, name, enabled, script) in scripts if enabled] + enabled_scripts = ['
  • %s
  • ' % s.name for s in iter_tagging_scripts_from_tuples(scripts) if s.enabled] if not enabled_scripts: return _("No enabled scripts") return _("Enabled scripts:") + '' diff --git a/picard/ui/options/scripting.py b/picard/ui/options/scripting.py index b75158112..d3508c0b1 100644 --- a/picard/ui/options/scripting.py +++ b/picard/ui/options/scripting.py @@ -40,10 +40,15 @@ from picard.i18n import ( N_, gettext as _, ) -from picard.script import ScriptParser +from picard.script import ( + ScriptParser, + TaggingScriptSetting, + iter_tagging_scripts_from_config, + save_tagging_scripts_to_config, +) from picard.script.serializer import ( - ScriptImportExportError, - TaggingScript, + ScriptSerializerImportExportError, + TaggingScriptInfo, ) from picard.ui import ( @@ -146,12 +151,12 @@ class ScriptingOptionsPage(OptionsPage): error_message = _(fmt) % params self.display_error(ScriptFileError(_(title), error_message)) - def output_file_error(self, error: ScriptImportExportError): + def output_file_error(self, error: ScriptSerializerImportExportError): """Log file error and display error message dialog. Args: fmt (str): Format for the error type being displayed - error (ScriptImportExportError): The error as a ScriptImportExportError instance + error (ScriptSerializerImportExportError): The error as a ScriptSerializerImportExportError instance """ params = { 'filename': error.filename, @@ -164,13 +169,14 @@ class ScriptingOptionsPage(OptionsPage): a Picard script package. """ try: - script_item = TaggingScript().import_script(self) - except ScriptImportExportError as error: + tagging_script = TaggingScriptInfo().import_script(self) + except ScriptSerializerImportExportError as error: self.output_file_error(error) return - if script_item: - title = _("%s (imported)") % script_item['title'] - list_item = ScriptListWidgetItem(title, False, script_item['script']) + if tagging_script: + title = _("%s (imported)") % tagging_script['title'] + script = TaggingScriptSetting(name=title, enabled=False, content=tagging_script['script']) + list_item = ScriptListWidgetItem(script) self.ui.script_list.addItem(list_item) self.ui.script_list.setCurrentRow(self.ui.script_list.count() - 1) @@ -178,19 +184,19 @@ class ScriptingOptionsPage(OptionsPage): """Export the current script to an external file. Export can be either as a plain text script or a naming script package. """ - items = self.ui.script_list.selectedItems() - if not items: + list_items = self.ui.script_list.selectedItems() + if not list_items: return - item = items[0] - script_text = item.script - script_title = item.name if item.name.strip() else _("Unnamed Script") - - if script_text: - script_item = TaggingScript(title=script_title, script=script_text) + list_item = list_items[0] + content = list_item.script.content + if content: + name = list_item.script.name.strip() + title = name or _("Unnamed Script") + tagging_script = TaggingScriptInfo(title=title, script=content) try: - script_item.export_script(parent=self) - except ScriptImportExportError as error: + tagging_script.export_script(parent=self) + except ScriptSerializerImportExportError as error: self.output_file_error(error) def enable_tagger_scripts_toggled(self, on): @@ -198,11 +204,11 @@ class ScriptingOptionsPage(OptionsPage): self.ui.script_list.add_script() def script_selected(self): - items = self.ui.script_list.selectedItems() - if items: - item = items[0] + list_items = self.ui.script_list.selectedItems() + if list_items: + list_item = list_items[0] self.ui.tagger_script.setEnabled(True) - self.ui.tagger_script.setText(item.script) + self.ui.tagger_script.setText(list_item.script.content) self.ui.tagger_script.setFocus(QtCore.Qt.FocusReason.OtherFocusReason) self.ui.export_button.setEnabled(True) else: @@ -211,21 +217,21 @@ class ScriptingOptionsPage(OptionsPage): self.ui.export_button.setEnabled(False) def live_update_and_check(self): - items = self.ui.script_list.selectedItems() - if not items: + list_items = self.ui.script_list.selectedItems() + if not list_items: return - script = items[0] - script.script = self.ui.tagger_script.toPlainText() + list_item = list_items[0] + list_item.script.content = self.ui.tagger_script.toPlainText() self.ui.script_error.setStyleSheet("") self.ui.script_error.setText("") try: self.check() except OptionsCheckError as e: - script.has_error = True + list_item.has_error = True self.ui.script_error.setStyleSheet(self.STYLESHEET_ERROR) self.ui.script_error.setText(e.info) return - script.has_error = False + list_item.has_error = False def reset_selected_item(self): widget = self.ui.script_list @@ -248,8 +254,8 @@ class ScriptingOptionsPage(OptionsPage): config = get_config() self.ui.enable_tagger_scripts.setChecked(config.setting['enable_tagger_scripts']) self.ui.script_list.clear() - for pos, name, enabled, text in config.setting['list_of_scripts']: - list_item = ScriptListWidgetItem(name, enabled, text) + for script in iter_tagging_scripts_from_config(config=config): + list_item = ScriptListWidgetItem(script) self.ui.script_list.addItem(list_item) # Select the last selected script item @@ -261,12 +267,12 @@ class ScriptingOptionsPage(OptionsPage): def _all_scripts(self): for item in qlistwidget_items(self.ui.script_list): - yield item.get_all() + yield item.get_script() def save(self): config = get_config() config.setting['enable_tagger_scripts'] = self.ui.enable_tagger_scripts.isChecked() - config.setting['list_of_scripts'] = list(self._all_scripts()) + save_tagging_scripts_to_config(self._all_scripts()) config.persist['last_selected_script_pos'] = self.ui.script_list.currentRow() def display_error(self, error): diff --git a/picard/ui/scripteditor.py b/picard/ui/scripteditor.py index 867ccc4bd..ade1697de 100644 --- a/picard/ui/scripteditor.py +++ b/picard/ui/scripteditor.py @@ -57,10 +57,11 @@ from picard.script import ( ScriptParser, get_file_naming_script, get_file_naming_script_presets, + iter_tagging_scripts_from_tuples, ) from picard.script.serializer import ( - FileNamingScript, - ScriptImportExportError, + FileNamingScriptInfo, + ScriptSerializerImportExportError, ) from picard.util import ( get_base_title, @@ -162,10 +163,10 @@ class ScriptEditorExamples(): try: # Only apply scripts if the original file metadata has not been changed. if self.settings['enable_tagger_scripts'] and not c_metadata.diff(file.orig_metadata): - for s_pos, s_name, s_enabled, s_text in self.settings['list_of_scripts']: - if s_enabled and s_text: + for s in iter_tagging_scripts_from_tuples(self.settings['list_of_scripts']): + if s.enabled and s.content: parser = ScriptParser() - parser.eval(s_text, c_metadata) + parser.eval(s.content, c_metadata) filename_before = file.filename filename_after = file.make_filename(filename_before, c_metadata, self.settings, self.script_text) if not self.settings['move_files']: @@ -385,7 +386,7 @@ def populate_script_selection_combo_box(naming_scripts, selected_script_id, comb """Populate the specified script selection combo box and identify the selected script. Args: - naming_scripts (dict): Dictionary of available user-defined naming scripts as script dictionaries as produced by FileNamingScript().to_dict() + naming_scripts (dict): Dictionary of available user-defined naming scripts as script dictionaries as produced by FileNamingScriptInfo().to_dict() selected_script_id (str): ID code for the currently selected script combo_box (QComboBox): Combo box object to populate @@ -935,7 +936,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog): """Insert a new item into the script selection combo box and update the script list in the settings. Args: - script_item (dict): File naming script to insert as produced by FileNamingScript().to_dict() + script_item (dict): File naming script to insert as produced by FileNamingScriptInfo().to_dict() """ self.selected_script_id = script_item['id'] self.naming_scripts[self.selected_script_id] = script_item @@ -959,7 +960,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog): def new_script(self, script): """Add a new script to the script selection combo box and script list. """ - script_item = FileNamingScript(script=script) + script_item = FileNamingScriptInfo(script=script) script_item.title = self.new_script_name() self._insert_item(script_item.to_dict()) @@ -967,7 +968,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog): """Add a copy of the script as a new editable script to the script selection combo box. """ selected, script_item = self.get_selected_index_and_item() - new_item = FileNamingScript.create_from_dict(script_dict=script_item).copy() + new_item = FileNamingScriptInfo.create_from_dict(script_dict=script_item).copy() base_title = "%s %s" % (get_base_title(script_item['title']), gettext_constants(DEFAULT_COPY_TEXT)) new_item.title = self.new_script_name(base_title) @@ -1008,7 +1009,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog): """Get the specified item from the script selection combo box. Returns: - dict: File naming script dictionary as produced by FileNamingScript().to_dict() + dict: File naming script dictionary as produced by FileNamingScriptInfo().to_dict() """ return self.get_script_item(self.ui.preset_naming_scripts.currentIndex()) @@ -1057,7 +1058,7 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog): Args: idx (int): Index of the item to update - script_item (dict): Updated file naming script information as produced by FileNamingScript().to_dict() + script_item (dict): Updated file naming script information as produced by FileNamingScriptInfo().to_dict() """ self.ui.preset_naming_scripts.setItemData(idx, script_item) title = script_item['title'] @@ -1232,8 +1233,8 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog): a naming script package. """ try: - script_item = FileNamingScript().import_script(self) - except ScriptImportExportError as error: + script_item = FileNamingScriptInfo().import_script(self) + except ScriptSerializerImportExportError as error: self.output_file_error(error.format, error.filename, error.error_msg) return if script_item: @@ -1276,11 +1277,11 @@ class ScriptEditorDialog(PicardDialog, SingletonDialog): script or a naming script package. """ selected = self.get_selected_item() - script_item = FileNamingScript.create_from_dict(script_dict=selected, create_new_id=False) + script_item = FileNamingScriptInfo.create_from_dict(script_dict=selected, create_new_id=False) script_item.title = get_base_title(script_item.title) try: script_item.export_script(parent=self) - except ScriptImportExportError as error: + except ScriptSerializerImportExportError as error: self.output_file_error(error.format, error.filename, error.error_msg) def check_formats(self): @@ -1401,7 +1402,7 @@ class ScriptDetailsEditor(PicardDialog): def set_last_updated(self): """Set the last updated value to the current timestamp. """ - self.ui.script_last_updated.setText(FileNamingScript.make_last_updated()) + self.ui.script_last_updated.setText(FileNamingScriptInfo.make_last_updated()) self.ui.script_last_updated.setModified(True) def save_changes(self): diff --git a/picard/ui/scriptsmenu.py b/picard/ui/scriptsmenu.py index 00f55b08b..11b991e39 100644 --- a/picard/ui/scriptsmenu.py +++ b/picard/ui/scriptsmenu.py @@ -46,23 +46,21 @@ class ScriptsMenu(QtWidgets.QMenu): super().__init__(*args) for script in scripts: - action = self.addAction(script[1]) + action = self.addAction(script.name) action.triggered.connect(partial(self._run_script, script)) def _run_script(self, script): - s_name = script[1] - s_text = script[3] parser = ScriptParser() for obj in self._iter_unique_metadata_objects(): try: - parser.eval(s_text, obj.metadata) + parser.eval(script.content, obj.metadata) obj.update() except ScriptError as e: - log.exception('Error running tagger script "%s" on object %r', s_name, obj) + log.exception('Error running tagger script "%s" on object %r', script.name, obj) msg = N_('Script error in "%(script)s": %(message)s') mparms = { - 'script': s_name, + 'script': script.name, 'message': str(e), } self.tagger.window.set_statusbar_message(msg, mparms) diff --git a/picard/ui/widgets/scriptlistwidget.py b/picard/ui/widgets/scriptlistwidget.py index 1b7706308..a39f184cc 100644 --- a/picard/ui/widgets/scriptlistwidget.py +++ b/picard/ui/widgets/scriptlistwidget.py @@ -35,6 +35,7 @@ from picard.i18n import ( gettext as _, gettext_constants, ) +from picard.script import TaggingScriptSetting from picard.util import unique_numbered_title from picard.ui import HashableListWidgetItem @@ -77,7 +78,7 @@ class ScriptListWidget(QtWidgets.QListWidget): def add_script(self): numbered_name = self.unique_script_name() - list_item = ScriptListWidgetItem(name=numbered_name) + list_item = ScriptListWidgetItem(TaggingScriptSetting(name=numbered_name, enabled=True)) list_item.setCheckState(QtCore.Qt.CheckState.Checked) self.addItem(list_item) self.setCurrentItem(list_item, QtCore.QItemSelectionModel.SelectionFlag.Clear @@ -114,14 +115,15 @@ class ScriptListWidget(QtWidgets.QListWidget): class ScriptListWidgetItem(HashableListWidgetItem): """Holds a script's list and text widget properties""" - def __init__(self, name=None, enabled=True, script=""): - super().__init__(name) + def __init__(self, script): + assert isinstance(script, TaggingScriptSetting) + super().__init__(script.name) self.setFlags(self.flags() | QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsEditable) - if name is None: - name = gettext_constants(DEFAULT_SCRIPT_NAME) - self.setText(name) - self.setCheckState(QtCore.Qt.CheckState.Checked if enabled else QtCore.Qt.CheckState.Unchecked) - self.script = script + if not script.name: + script.name = gettext_constants(DEFAULT_SCRIPT_NAME) + self.setText(script.name) + self.setCheckState(QtCore.Qt.CheckState.Checked if script.enabled else QtCore.Qt.CheckState.Unchecked) + self._script = script self.has_error = False @property @@ -136,6 +138,12 @@ class ScriptListWidgetItem(HashableListWidgetItem): def enabled(self): return self.checkState() == QtCore.Qt.CheckState.Checked - def get_all(self): - # tuples used to get pickle dump of settings to work - return (self.pos, self.name, self.enabled, self.script) + @property + def script(self): + return self._script + + def get_script(self): + self._script.pos = self.pos + self._script.name = self.name + self._script.enabled = self.enabled + return self._script diff --git a/test/test_script_serializer.py b/test/test_script_serializer.py index 943c06bf3..40f74f733 100644 --- a/test/test_script_serializer.py +++ b/test/test_script_serializer.py @@ -28,9 +28,9 @@ import yaml from test.picardtestcase import PicardTestCase from picard.script.serializer import ( - FileNamingScript, - PicardScript, - ScriptImportError, + FileNamingScriptInfo, + ScriptSerializer, + ScriptSerializerFromFileError, ) @@ -43,14 +43,14 @@ class MockDateTime(datetime.datetime): raise Exception("Unexpected parameter tz=%r" % tz) -class PicardScriptTest(PicardTestCase): +class ScriptSerializerTest(PicardTestCase): def assertYamlEquals(self, yaml_str, obj, msg=None): self.assertEqual(obj, yaml.safe_load(yaml_str), msg) def test_script_object_1(self): # Check initial loaded values. - test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0') + test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0') self.assertEqual(test_script.id, '12345') self.assertEqual(test_script['id'], '12345') self.assertEqual(test_script.last_updated, '2021-04-26') @@ -59,7 +59,7 @@ class PicardScriptTest(PicardTestCase): def test_script_object_2(self): # Check updating values directly so as not to modify `last_updated`. - test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26') + test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26') test_script.id = '54321' self.assertEqual(test_script.id, '54321') self.assertEqual(test_script['id'], '54321') @@ -73,7 +73,7 @@ class PicardScriptTest(PicardTestCase): def test_script_object_3(self): # Check updating values that are ignored from modifying `last_updated`. - test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26') + test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26') test_script.update_script_setting(id='54321') self.assertEqual(test_script.id, '54321') self.assertEqual(test_script['id'], '54321') @@ -83,7 +83,7 @@ class PicardScriptTest(PicardTestCase): @patch('datetime.datetime', MockDateTime) def test_script_object_4(self): # Check updating values that modify `last_updated`. - test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26') + test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26') test_script.update_script_setting(title='Updated Script 1') self.assertEqual(test_script.title, 'Updated Script 1') self.assertEqual(test_script['title'], 'Updated Script 1') @@ -93,7 +93,7 @@ class PicardScriptTest(PicardTestCase): @patch('datetime.datetime', MockDateTime) def test_script_object_5(self): # Check updating values from dict that modify `last_updated`. - test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26') + test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26') test_script.update_from_dict({"script": "Updated script"}) self.assertEqual(test_script.script, 'Updated script') self.assertEqual(test_script['script'], 'Updated script') @@ -102,7 +102,7 @@ class PicardScriptTest(PicardTestCase): def test_script_object_6(self): # Test that extra (unknown) settings are ignored during updating - test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0') + test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0') test_script.update_script_setting(description='Updated description') self.assertEqual(test_script['last_updated'], '2021-04-26') self.assertYamlEquals(test_script.to_yaml(), {"id": "12345", "script": "Script text\n", "script_language_version": "1.0", "title": "Script 1"}) @@ -111,7 +111,7 @@ class PicardScriptTest(PicardTestCase): def test_script_object_7(self): # Test that extra (unknown) settings are ignored during updating from dict - test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0') + test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0') test_script.update_from_dict({"description": "Updated description"}) self.assertEqual(test_script['last_updated'], '2021-04-26') self.assertYamlEquals(test_script.to_yaml(), {"id": "12345", "script": "Script text\n", "script_language_version": "1.0", "title": "Script 1"}) @@ -120,18 +120,18 @@ class PicardScriptTest(PicardTestCase): def test_script_object_8(self): # Test that requested unknown settings return None - test_script = PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26') + test_script = ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26') self.assertEqual(test_script['unknown_setting'], None) def test_script_object_9(self): # Test that an exception is raised when creating or updating using an invalid YAML string - with self.assertRaises(ScriptImportError): - PicardScript().create_from_yaml('Not a YAML string') - PicardScript(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0') + with self.assertRaises(ScriptSerializerFromFileError): + ScriptSerializer().create_from_yaml('Not a YAML string') + ScriptSerializer(title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', script_language_version='1.0') def test_naming_script_object_1(self): # Check initial loaded values. - test_script = FileNamingScript( + test_script = FileNamingScriptInfo( title='Script 1', script='Script text', id='12345', last_updated='2021-04-26', description='Script description', author='Script author', script_language_version='1.0' )