From 05e6eaf88e12c0500b4ecb6ea946daf08979ed89 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Tue, 5 Sep 2023 03:55:12 -0400 Subject: [PATCH] Update setting group names Make group names presentable to users and add builtin plugins during namespace generation. Revamp talkeruigenerator.py to use generated group and setting names and remove as much hard-coded strings as possible Add a --list-plugins commandline option --- build-tools/generate_settngs.py | 1 + comictaggerlib/autotagmatchwindow.py | 10 +- comictaggerlib/autotagstartwindow.py | 26 +-- comictaggerlib/cbltransformer.py | 14 +- comictaggerlib/cli.py | 160 +++++++++--------- comictaggerlib/ctsettings/__init__.py | 3 +- comictaggerlib/ctsettings/commandline.py | 59 ++++--- comictaggerlib/ctsettings/file.py | 32 ++-- comictaggerlib/ctsettings/plugin.py | 40 +++-- .../ctsettings/settngs_namespace.py | 153 +++++++++-------- comictaggerlib/gui.py | 6 +- comictaggerlib/issueidentifier.py | 18 +- comictaggerlib/issueselectionwindow.py | 7 +- comictaggerlib/main.py | 36 ++-- comictaggerlib/matchselectionwindow.py | 2 +- comictaggerlib/renamewindow.py | 30 ++-- comictaggerlib/seriesselectionwindow.py | 18 +- comictaggerlib/settingswindow.py | 132 ++++++++------- comictaggerlib/taggerwindow.py | 65 +++---- comictaggerlib/ui/talkeruigenerator.py | 150 ++++++++-------- setup.cfg | 2 +- tests/comiccacher_test.py | 4 +- tests/conftest.py | 14 +- 23 files changed, 532 insertions(+), 450 deletions(-) diff --git a/build-tools/generate_settngs.py b/build-tools/generate_settngs.py index 6fd90d0..c196943 100644 --- a/build-tools/generate_settngs.py +++ b/build-tools/generate_settngs.py @@ -9,6 +9,7 @@ import comictaggerlib.main def generate() -> str: app = comictaggerlib.main.App() + app.load_plugins(app.initial_arg_parser.parse_known_args()[0]) app.register_settings() return settngs.generate_ns(app.manager.definitions) diff --git a/comictaggerlib/autotagmatchwindow.py b/comictaggerlib/autotagmatchwindow.py index 144e4f7..e492373 100644 --- a/comictaggerlib/autotagmatchwindow.py +++ b/comictaggerlib/autotagmatchwindow.py @@ -52,7 +52,7 @@ class AutoTagMatchWindow(QtWidgets.QDialog): self.current_match_set: MultipleMatch = match_set_list[0] self.altCoverWidget = CoverImageWidget( - self.altCoverContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker + self.altCoverContainer, CoverImageWidget.AltCoverMode, config.Runtime_Options_config.user_cache_dir, talker ) gridlayout = QtWidgets.QGridLayout(self.altCoverContainer) gridlayout.addWidget(self.altCoverWidget) @@ -233,10 +233,10 @@ class AutoTagMatchWindow(QtWidgets.QDialog): md = ca.read_metadata(self._style) if md.is_empty: md = ca.metadata_from_filename( - self.config.filename_complicated_parser, - self.config.filename_remove_c2c, - self.config.filename_remove_fcbd, - self.config.filename_remove_publisher, + self.config.Filename_Parsing_complicated_parser, + self.config.Filename_Parsing_remove_c2c, + self.config.Filename_Parsing_remove_fcbd, + self.config.Filename_Parsing_remove_publisher, ) # now get the particular issue data diff --git a/comictaggerlib/autotagstartwindow.py b/comictaggerlib/autotagstartwindow.py index 1f8f138..44c2949 100644 --- a/comictaggerlib/autotagstartwindow.py +++ b/comictaggerlib/autotagstartwindow.py @@ -40,15 +40,15 @@ class AutoTagStartWindow(QtWidgets.QDialog): self.cbxSpecifySearchString.setChecked(False) self.cbxSplitWords.setChecked(False) - self.sbNameMatchSearchThresh.setValue(self.config.identifier_series_match_identify_thresh) + self.sbNameMatchSearchThresh.setValue(self.config.Issue_Identifier_series_match_identify_thresh) self.leSearchString.setEnabled(False) - self.cbxSaveOnLowConfidence.setChecked(self.config.autotag_save_on_low_confidence) - self.cbxDontUseYear.setChecked(self.config.autotag_dont_use_year_when_identifying) - self.cbxAssumeIssueOne.setChecked(self.config.autotag_assume_1_if_no_issue_num) - self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.config.autotag_ignore_leading_numbers_in_filename) - self.cbxRemoveAfterSuccess.setChecked(self.config.autotag_remove_archive_after_successful_match) - self.cbxAutoImprint.setChecked(self.config.identifier_auto_imprint) + self.cbxSaveOnLowConfidence.setChecked(self.config.Auto_Tag_save_on_low_confidence) + self.cbxDontUseYear.setChecked(self.config.Auto_Tag_dont_use_year_when_identifying) + self.cbxAssumeIssueOne.setChecked(self.config.Auto_Tag_assume_1_if_no_issue_num) + self.cbxIgnoreLeadingDigitsInFilename.setChecked(self.config.Auto_Tag_ignore_leading_numbers_in_filename) + self.cbxRemoveAfterSuccess.setChecked(self.config.Auto_Tag_remove_archive_after_successful_match) + self.cbxAutoImprint.setChecked(self.config.Issue_Identifier_auto_imprint) nlmt_tip = """The Name Match Ratio Threshold: Auto-Identify is for eliminating automatic search matches that are too long compared to your series name search. The lower @@ -73,7 +73,7 @@ class AutoTagStartWindow(QtWidgets.QDialog): self.ignore_leading_digits_in_filename = False self.remove_after_success = False self.search_string = "" - self.name_length_match_tolerance = self.config.identifier_series_match_search_thresh + self.name_length_match_tolerance = self.config.Issue_Identifier_series_match_search_thresh self.split_words = self.cbxSplitWords.isChecked() def search_string_toggle(self) -> None: @@ -92,11 +92,11 @@ class AutoTagStartWindow(QtWidgets.QDialog): self.split_words = self.cbxSplitWords.isChecked() # persist some settings - self.config.autotag_save_on_low_confidence = self.auto_save_on_low - self.config.autotag_dont_use_year_when_identifying = self.dont_use_year - self.config.autotag_assume_1_if_no_issue_num = self.assume_issue_one - self.config.autotag_ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename - self.config.autotag_remove_archive_after_successful_match = self.remove_after_success + self.config.Auto_Tag_save_on_low_confidence = self.auto_save_on_low + self.config.Auto_Tag_dont_use_year_when_identifying = self.dont_use_year + self.config.Auto_Tag_assume_1_if_no_issue_num = self.assume_issue_one + self.config.Auto_Tag_ignore_leading_numbers_in_filename = self.ignore_leading_digits_in_filename + self.config.Auto_Tag_remove_archive_after_successful_match = self.remove_after_success if self.cbxSpecifySearchString.isChecked(): self.search_string = self.leSearchString.text() diff --git a/comictaggerlib/cbltransformer.py b/comictaggerlib/cbltransformer.py index fb65058..e52bf5f 100644 --- a/comictaggerlib/cbltransformer.py +++ b/comictaggerlib/cbltransformer.py @@ -29,7 +29,7 @@ class CBLTransformer: self.config = config def apply(self) -> GenericMetadata: - if self.config.cbl_assume_lone_credit_is_primary: + if self.config.Comic_Book_Lover_assume_lone_credit_is_primary: # helper def set_lone_primary(role_list: list[str]) -> tuple[Credit | None, int]: lone_credit: Credit | None = None @@ -55,19 +55,19 @@ class CBLTransformer: c["primary"] = False self.metadata.add_credit(c["person"], "Artist", True) - if self.config.cbl_copy_characters_to_tags: + if self.config.Comic_Book_Lover_copy_characters_to_tags: self.metadata.tags.update(x for x in self.metadata.characters) - if self.config.cbl_copy_teams_to_tags: + if self.config.Comic_Book_Lover_copy_teams_to_tags: self.metadata.tags.update(x for x in self.metadata.teams) - if self.config.cbl_copy_locations_to_tags: + if self.config.Comic_Book_Lover_copy_locations_to_tags: self.metadata.tags.update(x for x in self.metadata.locations) - if self.config.cbl_copy_storyarcs_to_tags: + if self.config.Comic_Book_Lover_copy_storyarcs_to_tags: self.metadata.tags.update(x for x in self.metadata.story_arcs) - if self.config.cbl_copy_notes_to_comments: + if self.config.Comic_Book_Lover_copy_notes_to_comments: if self.metadata.notes is not None: if self.metadata.description is None: self.metadata.description = "" @@ -76,7 +76,7 @@ class CBLTransformer: if self.metadata.notes not in self.metadata.description: self.metadata.description += self.metadata.notes - if self.config.cbl_copy_weblink_to_comments: + if self.config.Comic_Book_Lover_copy_weblink_to_comments: if self.metadata.web_link is not None: if self.metadata.description is None: self.metadata.description = "" diff --git a/comictaggerlib/cli.py b/comictaggerlib/cli.py index cbddcea..3eb1a66 100644 --- a/comictaggerlib/cli.py +++ b/comictaggerlib/cli.py @@ -46,9 +46,9 @@ class CLI: self.batch_mode = False def current_talker(self) -> ComicTalker: - if self.config.talker_source in self.talkers: - return self.talkers[self.config.talker_source] - logger.error("Could not find the '%s' talker", self.config.talker_source) + if self.config.Sources_source in self.talkers: + return self.talkers[self.config.Sources_source] + logger.error("Could not find the '%s' talker", self.config.Sources_source) raise SystemExit(2) def actual_issue_data_fetch(self, issue_id: str) -> GenericMetadata: @@ -59,14 +59,14 @@ class CLI: logger.exception(f"Error retrieving issue details. Save aborted.\n{e}") return GenericMetadata() - if self.config.cbl_apply_transform_on_import: + if self.config.Comic_Book_Lover_apply_transform_on_import: ct_md = CBLTransformer(ct_md, self.config).apply() return ct_md def actual_metadata_save(self, ca: ComicArchive, md: GenericMetadata) -> bool: - if not self.config.runtime_dryrun: - for metadata_style in self.config.runtime_type: + if not self.config.Runtime_Options_dryrun: + for metadata_style in self.config.Runtime_Options_type: # write out the new data if not ca.write_metadata(md, metadata_style): logger.error("The tag save seemed to fail for style: %s!", MetaDataStyle.name[metadata_style]) @@ -75,7 +75,7 @@ class CLI: print("Save complete.") logger.info("Save complete.") else: - if self.config.runtime_quiet: + if self.config.Runtime_Options_quiet: logger.info("dry-run option was set, so nothing was written") print("dry-run option was set, so nothing was written") else: @@ -102,7 +102,7 @@ class CLI: m["issue_title"], ) ) - if self.config.runtime_interactive: + if self.config.Runtime_Options_interactive: while True: i = input("Choose a match #, or 's' to skip: ") if (i.isdigit() and int(i) in range(1, len(match_set.matches) + 1)) or i == "s": @@ -113,7 +113,7 @@ class CLI: ca = match_set.ca md = self.create_local_metadata(ca) ct_md = self.actual_issue_data_fetch(match_set.matches[int(i) - 1]["issue_id"]) - if self.config.identifier_clear_metadata_on_import: + if self.config.Issue_Identifier_clear_metadata_on_import: md = ct_md else: notes = ( @@ -122,14 +122,14 @@ class CLI: ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config.identifier_auto_imprint: + if self.config.Issue_Identifier_auto_imprint: md.fix_publisher() self.actual_metadata_save(ca, md) def post_process_matches(self, match_results: OnlineMatchResults) -> None: # now go through the match results - if self.config.runtime_summary: + if self.config.Runtime_Options_summary: if len(match_results.good_matches) > 0: print("\nSuccessful matches:\n------------------") for f in match_results.good_matches: @@ -150,7 +150,7 @@ class CLI: for f in match_results.fetch_data_failures: print(f) - if not self.config.runtime_summary and not self.config.runtime_interactive: + if not self.config.Runtime_Options_summary and not self.config.Runtime_Options_interactive: # just quit if we're not interactive or showing the summary return @@ -170,14 +170,14 @@ class CLI: self.display_match_set_for_choice(label, match_set) def run(self) -> None: - if len(self.config.runtime_files) < 1: + if len(self.config.Runtime_Options_files) < 1: logger.error("You must specify at least one filename. Use the -h option for more info") return match_results = OnlineMatchResults() - self.batch_mode = len(self.config.runtime_files) > 1 + self.batch_mode = len(self.config.Runtime_Options_files) > 1 - for f in self.config.runtime_files: + for f in self.config.Runtime_Options_files: self.process_file_cli(f, match_results) sys.stdout.flush() @@ -190,18 +190,18 @@ class CLI: md.set_default_page_list(ca.get_number_of_pages()) # now, overlay the parsed filename info - if self.config.runtime_parse_filename: + if self.config.Runtime_Options_parse_filename: f_md = ca.metadata_from_filename( - self.config.filename_complicated_parser, - self.config.filename_remove_c2c, - self.config.filename_remove_fcbd, - self.config.filename_remove_publisher, - self.config.runtime_split_words, + self.config.Filename_Parsing_complicated_parser, + self.config.Filename_Parsing_remove_c2c, + self.config.Filename_Parsing_remove_fcbd, + self.config.Filename_Parsing_remove_publisher, + self.config.Runtime_Options_split_words, ) md.overlay(f_md) - for metadata_style in self.config.runtime_type: + for metadata_style in self.config.Runtime_Options_type: if ca.has_metadata(metadata_style): try: t_md = ca.read_metadata(metadata_style) @@ -211,12 +211,12 @@ class CLI: logger.error("Failed to load metadata for %s: %s", ca.path, e) # finally, use explicit stuff - md.overlay(self.config.runtime_metadata) + md.overlay(self.config.Runtime_Options_metadata) return md def print(self, ca: ComicArchive) -> None: - if not self.config.runtime_type: + if not self.config.Runtime_Options_type: page_count = ca.get_number_of_pages() brief = "" @@ -246,38 +246,38 @@ class CLI: print(brief) - if self.config.runtime_quiet: + if self.config.Runtime_Options_quiet: return print() - if not self.config.runtime_type or MetaDataStyle.CIX in self.config.runtime_type: + if not self.config.Runtime_Options_type or MetaDataStyle.CIX in self.config.Runtime_Options_type: if ca.has_metadata(MetaDataStyle.CIX): print("--------- ComicRack tags ---------") try: - if self.config.runtime_raw: + if self.config.Runtime_Options_raw: print(ca.read_raw_cix()) else: print(ca.read_cix()) except Exception as e: logger.error("Failed to load metadata for %s: %s", ca.path, e) - if not self.config.runtime_type or MetaDataStyle.CBI in self.config.runtime_type: + if not self.config.Runtime_Options_type or MetaDataStyle.CBI in self.config.Runtime_Options_type: if ca.has_metadata(MetaDataStyle.CBI): print("------- ComicBookLover tags -------") try: - if self.config.runtime_raw: + if self.config.Runtime_Options_raw: pprint(json.loads(ca.read_raw_cbi())) else: print(ca.read_cbi()) except Exception as e: logger.error("Failed to load metadata for %s: %s", ca.path, e) - if not self.config.runtime_type or MetaDataStyle.COMET in self.config.runtime_type: + if not self.config.Runtime_Options_type or MetaDataStyle.COMET in self.config.Runtime_Options_type: if ca.has_metadata(MetaDataStyle.COMET): print("----------- CoMet tags -----------") try: - if self.config.runtime_raw: + if self.config.Runtime_Options_raw: print(ca.read_raw_comet()) else: print(ca.read_comet()) @@ -285,10 +285,10 @@ class CLI: logger.error("Failed to load metadata for %s: %s", ca.path, e) def delete(self, ca: ComicArchive) -> None: - for metadata_style in self.config.runtime_type: + for metadata_style in self.config.Runtime_Options_type: style_name = MetaDataStyle.name[metadata_style] if ca.has_metadata(metadata_style): - if not self.config.runtime_dryrun: + if not self.config.Runtime_Options_dryrun: if not ca.remove_metadata(metadata_style): print(f"{ca.path}: Tag removal seemed to fail!") else: @@ -299,25 +299,25 @@ class CLI: print(f"{ca.path}: This archive doesn't have {style_name} tags to remove.") def copy(self, ca: ComicArchive) -> None: - for metadata_style in self.config.runtime_type: + for metadata_style in self.config.Runtime_Options_type: dst_style_name = MetaDataStyle.name[metadata_style] - if not self.config.runtime_overwrite and ca.has_metadata(metadata_style): + if not self.config.Runtime_Options_overwrite and ca.has_metadata(metadata_style): print(f"{ca.path}: Already has {dst_style_name} tags. Not overwriting.") return - if self.config.commands_copy == metadata_style: + if self.config.Commands_copy == metadata_style: print(f"{ca.path}: Destination and source are same: {dst_style_name}. Nothing to do.") return - src_style_name = MetaDataStyle.name[self.config.commands_copy] - if ca.has_metadata(self.config.commands_copy): - if not self.config.runtime_dryrun: + src_style_name = MetaDataStyle.name[self.config.Commands_copy] + if ca.has_metadata(self.config.Commands_copy): + if not self.config.Runtime_Options_dryrun: try: - md = ca.read_metadata(self.config.commands_copy) + md = ca.read_metadata(self.config.Commands_copy) except Exception as e: md = GenericMetadata() logger.error("Failed to load metadata for %s: %s", ca.path, e) - if self.config.cbl_apply_transform_on_bulk_operation == MetaDataStyle.CBI: + if self.config.Comic_Book_Lover_apply_transform_on_bulk_operation == MetaDataStyle.CBI: md = CBLTransformer(md, self.config).apply() if not ca.write_metadata(md, metadata_style): @@ -330,8 +330,8 @@ class CLI: print(f"{ca.path}: This archive doesn't have {src_style_name} tags to copy.") def save(self, ca: ComicArchive, match_results: OnlineMatchResults) -> None: - if not self.config.runtime_overwrite: - for metadata_style in self.config.runtime_type: + if not self.config.Runtime_Options_overwrite: + for metadata_style in self.config.Runtime_Options_type: if ca.has_metadata(metadata_style): print(f"{ca.path}: Already has {MetaDataStyle.name[metadata_style]} tags. Not overwriting.") return @@ -341,26 +341,26 @@ class CLI: md = self.create_local_metadata(ca) if md.issue is None or md.issue == "": - if self.config.autotag_assume_1_if_no_issue_num: + if self.config.Auto_Tag_assume_1_if_no_issue_num: md.issue = "1" # now, search online - if self.config.runtime_online: - if self.config.runtime_issue_id is not None: + if self.config.Runtime_Options_online: + if self.config.Runtime_Options_issue_id is not None: # we were given the actual issue ID to search with try: - ct_md = self.current_talker().fetch_comic_data(self.config.runtime_issue_id) + ct_md = self.current_talker().fetch_comic_data(self.config.Runtime_Options_issue_id) except TalkerError as e: logger.exception(f"Error retrieving issue details. Save aborted.\n{e}") match_results.fetch_data_failures.append(str(ca.path.absolute())) return if ct_md is None: - logger.error("No match for ID %s was found.", self.config.runtime_issue_id) + logger.error("No match for ID %s was found.", self.config.Runtime_Options_issue_id) match_results.no_matches.append(str(ca.path.absolute())) return - if self.config.cbl_apply_transform_on_import: + if self.config.Comic_Book_Lover_apply_transform_on_import: ct_md = CBLTransformer(ct_md, self.config).apply() else: if md is None or md.is_empty: @@ -371,7 +371,7 @@ class CLI: ii = IssueIdentifier(ca, self.config, self.current_talker()) def myoutput(text: str) -> None: - if self.config.runtime_verbose: + if self.config.Runtime_Options_verbose: IssueIdentifier.default_write_output(text) # use our overlaid MD struct to search @@ -411,7 +411,7 @@ class CLI: logger.error("Online search: Multiple good matches. Save aborted") match_results.multiple_matches.append(MultipleMatch(ca, matches)) return - if low_confidence and self.config.runtime_abort_on_low_confidence: + if low_confidence and self.config.Runtime_Options_abort_on_low_confidence: logger.error("Online search: Low confidence match. Save aborted") match_results.low_confidence_matches.append(MultipleMatch(ca, matches)) return @@ -428,7 +428,7 @@ class CLI: match_results.fetch_data_failures.append(str(ca.path.absolute())) return - if self.config.identifier_clear_metadata_on_import: + if self.config.Issue_Identifier_clear_metadata_on_import: md = GenericMetadata() notes = ( @@ -438,11 +438,11 @@ class CLI: md.overlay( ct_md.replace( notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"), - description=cleanup_html(ct_md.description, self.config.talker_remove_html_tables), + description=cleanup_html(ct_md.description, self.config.Sources_remove_html_tables), ) ) - if self.config.identifier_auto_imprint: + if self.config.Issue_Identifier_auto_imprint: md.fix_publisher() # ok, done building our metadata. time to save @@ -464,18 +464,18 @@ class CLI: return new_ext = "" # default - if self.config.rename_set_extension_based_on_archive: + if self.config.File_Rename_set_extension_based_on_archive: new_ext = ca.extension() renamer = FileRenamer( md, - platform="universal" if self.config.rename_strict else "auto", - replacements=self.config.rename_replacements, + platform="universal" if self.config.File_Rename_strict else "auto", + replacements=self.config.File_Rename_replacements, ) - renamer.set_template(self.config.rename_template) - renamer.set_issue_zero_padding(self.config.rename_issue_number_padding) - renamer.set_smart_cleanup(self.config.rename_use_smart_string_cleanup) - renamer.move = self.config.rename_move_to_dir + renamer.set_template(self.config.File_Rename_template) + renamer.set_issue_zero_padding(self.config.File_Rename_issue_number_padding) + renamer.set_smart_cleanup(self.config.File_Rename_use_smart_string_cleanup) + renamer.move = self.config.File_Rename_move_to_dir try: new_name = renamer.determine_name(ext=new_ext) @@ -487,14 +487,14 @@ class CLI: "Please consult the template help in the settings " "and the documentation on the format at " "https://docs.python.org/3/library/string.html#format-string-syntax", - self.config.rename_template, + self.config.File_Rename_template, ) return except Exception: - logger.exception("Formatter failure: %s metadata: %s", self.config.rename_template, renamer.metadata) + logger.exception("Formatter failure: %s metadata: %s", self.config.File_Rename_template, renamer.metadata) return - folder = get_rename_dir(ca, self.config.rename_dir if self.config.rename_move_to_dir else None) + folder = get_rename_dir(ca, self.config.File_Rename_dir if self.config.File_Rename_move_to_dir else None) full_path = folder / new_name @@ -503,7 +503,7 @@ class CLI: return suffix = "" - if not self.config.runtime_dryrun: + if not self.config.Runtime_Options_dryrun: # rename the file try: ca.rename(utils.unique_file(full_path)) @@ -526,7 +526,7 @@ class CLI: filename_path = ca.path new_file = filename_path.with_suffix(".cbz") - if self.config.runtime_abort_on_conflict and new_file.exists(): + if self.config.Runtime_Options_abort_on_conflict and new_file.exists(): print(msg_hdr + f"{new_file.name} already exists in the that folder.") return @@ -534,10 +534,10 @@ class CLI: delete_success = False export_success = False - if not self.config.runtime_dryrun: + if not self.config.Runtime_Options_dryrun: if ca.export_as_zip(new_file): export_success = True - if self.config.runtime_delete_after_zip_export: + if self.config.Runtime_Options_delete_after_zip_export: try: filename_path.unlink(missing_ok=True) delete_success = True @@ -549,7 +549,7 @@ class CLI: new_file.unlink(missing_ok=True) else: msg = msg_hdr + f"Dry-run: Would try to create {os.path.split(new_file)[1]}" - if self.config.runtime_delete_after_zip_export: + if self.config.Runtime_Options_delete_after_zip_export: msg += " and delete original." print(msg) return @@ -557,7 +557,7 @@ class CLI: msg = msg_hdr if export_success: msg += f"Archive exported successfully to: {os.path.split(new_file)[1]}" - if self.config.runtime_delete_after_zip_export and delete_success: + if self.config.Runtime_Options_delete_after_zip_export and delete_success: msg += " (Original deleted) " else: msg += "Archive failed to export!" @@ -576,28 +576,28 @@ class CLI: return if not ca.is_writable() and ( - self.config.commands_delete - or self.config.commands_copy - or self.config.commands_save - or self.config.commands_rename + self.config.Commands_delete + or self.config.Commands_copy + or self.config.Commands_save + or self.config.Commands_rename ): logger.error("This archive is not writable") return - if self.config.commands_print: + if self.config.Commands_print: self.print(ca) - elif self.config.commands_delete: + elif self.config.Commands_delete: self.delete(ca) - elif self.config.commands_copy is not None: + elif self.config.Commands_copy is not None: self.copy(ca) - elif self.config.commands_save: + elif self.config.Commands_save: self.save(ca, match_results) - elif self.config.commands_rename: + elif self.config.Commands_rename: self.rename(ca) - elif self.config.commands_export_to_zip: + elif self.config.Commands_export_to_zip: self.export(ca) diff --git a/comictaggerlib/ctsettings/__init__.py b/comictaggerlib/ctsettings/__init__.py index d440907..632d8ec 100644 --- a/comictaggerlib/ctsettings/__init__.py +++ b/comictaggerlib/ctsettings/__init__.py @@ -6,7 +6,7 @@ from comictaggerlib.ctsettings.commandline import ( validate_commandline_settings, ) from comictaggerlib.ctsettings.file import register_file_settings, validate_file_settings -from comictaggerlib.ctsettings.plugin import register_plugin_settings, validate_plugin_settings +from comictaggerlib.ctsettings.plugin import group_for_plugin, register_plugin_settings, validate_plugin_settings from comictaggerlib.ctsettings.settngs_namespace import settngs_namespace as ct_ns from comictaggerlib.ctsettings.types import ComicTaggerPaths from comictalker import ComicTalker @@ -23,4 +23,5 @@ __all__ = [ "validate_plugin_settings", "ComicTaggerPaths", "ct_ns", + "group_for_plugin", ] diff --git a/comictaggerlib/ctsettings/commandline.py b/comictaggerlib/ctsettings/commandline.py index 8f10eef..02841df 100644 --- a/comictaggerlib/ctsettings/commandline.py +++ b/comictaggerlib/ctsettings/commandline.py @@ -236,61 +236,72 @@ def register_commands(parser: settngs.Manager) -> None: parser.add_setting( "--only-set-cv-key", action="store_true", - help="Only set the Comic Vine API key and quit.\n\n", + help="Only set the Comic Vine API key and quit.", + file=False, + ) + parser.add_setting( + "--list-plugins", + action="store_true", + help="List the available plugins.\n\n", file=False, ) def register_commandline_settings(parser: settngs.Manager) -> None: - parser.add_group("commands", register_commands, True) - parser.add_persistent_group("runtime", register_runtime) + parser.add_group("Commands", register_commands, True) + parser.add_persistent_group("Runtime Options", register_runtime) def validate_commandline_settings(config: settngs.Config[ct_ns], parser: settngs.Manager) -> settngs.Config[ct_ns]: - if config[0].commands_version: + if config[0].Commands_version: parser.exit( status=1, message=f"ComicTagger {ctversion.version}: Copyright (c) 2012-2022 ComicTagger Team\n" "Distributed under Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)\n", ) - config[0].runtime_no_gui = any( + config[0].Runtime_Options_no_gui = any( [ - config[0].commands_print, - config[0].commands_delete, - config[0].commands_save, - config[0].commands_copy, - config[0].commands_rename, - config[0].commands_export_to_zip, - config[0].commands_only_set_cv_key, - config[0].runtime_no_gui, + config[0].Commands_print, + config[0].Commands_delete, + config[0].Commands_save, + config[0].Commands_copy, + config[0].Commands_rename, + config[0].Commands_export_to_zip, + config[0].Commands_only_set_cv_key, + config[0].Commands_list_plugins, + config[0].Runtime_Options_no_gui, ] ) - if platform.system() == "Windows" and config[0].runtime_glob: + if platform.system() == "Windows" and config[0].Runtime_Options_glob: # no globbing on windows shell, so do it for them import glob - globs = config[0].runtime_files - config[0].runtime_files = [] + globs = config[0].Runtime_Options_files + config[0].Runtime_Options_files = [] for item in globs: - config[0].runtime_files.extend(glob.glob(item)) + config[0].Runtime_Options_files.extend(glob.glob(item)) - if not config[0].commands_only_set_cv_key and config[0].runtime_no_gui and not config[0].runtime_files: + if ( + not config[0].Commands_only_set_cv_key + and config[0].Runtime_Options_no_gui + and not config[0].Runtime_Options_files + ): parser.exit(message="Command requires at least one filename!\n", status=1) - if config[0].commands_delete and not config[0].runtime_type: + if config[0].Commands_delete and not config[0].Runtime_Options_type: parser.exit(message="Please specify the type to delete with -t\n", status=1) - if config[0].commands_save and not config[0].runtime_type: + if config[0].Commands_save and not config[0].Runtime_Options_type: parser.exit(message="Please specify the type to save with -t\n", status=1) - if config[0].commands_copy: - if not config[0].runtime_type: + if config[0].Commands_copy: + if not config[0].Runtime_Options_type: parser.exit(message="Please specify the type to copy to with -t\n", status=1) - if config[0].runtime_recursive: - config[0].runtime_files = utils.get_recursive_filelist(config[0].runtime_files) + if config[0].Runtime_Options_recursive: + config[0].Runtime_Options_files = utils.get_recursive_filelist(config[0].Runtime_Options_files) # take a crack at finding rar exe if it's not in the path if not utils.which("rar"): diff --git a/comictaggerlib/ctsettings/file.py b/comictaggerlib/ctsettings/file.py index 42cd91b..5a3e0db 100644 --- a/comictaggerlib/ctsettings/file.py +++ b/comictaggerlib/ctsettings/file.py @@ -123,7 +123,11 @@ def filename(parser: settngs.Manager) -> None: def talker(parser: settngs.Manager) -> None: # General settings for talkers - parser.add_setting("--source", default="comicvine", help="Use a specified source by source ID") + parser.add_setting( + "--source", + default="comicvine", + help="Use a specified source by source ID (use --list-plugins to list all sources)", + ) parser.add_setting( "--remove-html-tables", default=False, @@ -219,7 +223,7 @@ def autotag(parser: settngs.Manager) -> None: def validate_file_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]: new_filter = [] remove = [] - for x in config[0].identifier_publisher_filter: + for x in config[0].Issue_Identifier_publisher_filter: x = x.strip() if x: # ignore empty arguments if x[-1] == "-": # this publisher needs to be removed. We remove after all publishers have been enumerated @@ -230,22 +234,22 @@ def validate_file_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_n for x in remove: # remove publishers if x in new_filter: new_filter.remove(x) - config[0].identifier_publisher_filter = new_filter + config[0].Issue_Identifier_publisher_filter = new_filter - config[0].rename_replacements = Replacements( - [Replacement(x[0], x[1], x[2]) for x in config[0].rename_replacements[0]], - [Replacement(x[0], x[1], x[2]) for x in config[0].rename_replacements[1]], + config[0].File_Rename_replacements = Replacements( + [Replacement(x[0], x[1], x[2]) for x in config[0].File_Rename_replacements[0]], + [Replacement(x[0], x[1], x[2]) for x in config[0].File_Rename_replacements[1]], ) return config def register_file_settings(parser: settngs.Manager) -> None: - parser.add_group("general", general, False) parser.add_group("internal", internal, False) - parser.add_group("identifier", identifier, False) - parser.add_group("dialog", dialog, False) - parser.add_group("filename", filename, False) - parser.add_group("talker", talker, False) - parser.add_group("cbl", cbl, False) - parser.add_group("rename", rename, False) - parser.add_group("autotag", autotag, False) + parser.add_group("Issue Identifier", identifier, False) + parser.add_group("Filename Parsing", filename, False) + parser.add_group("Sources", talker, False) + parser.add_group("Comic Book Lover", cbl, False) + parser.add_group("File Rename", rename, False) + parser.add_group("Auto-Tag", autotag, False) + parser.add_group("General", general, False) + parser.add_group("Dialog Flags", dialog, False) diff --git a/comictaggerlib/ctsettings/plugin.py b/comictaggerlib/ctsettings/plugin.py index 50fc82a..dc6d820 100644 --- a/comictaggerlib/ctsettings/plugin.py +++ b/comictaggerlib/ctsettings/plugin.py @@ -7,12 +7,23 @@ from typing import cast import settngs import comicapi.comicarchive +import comicapi.utils import comictaggerlib.ctsettings +from comicapi.comicarchive import Archiver from comictaggerlib.ctsettings.settngs_namespace import settngs_namespace as ct_ns +from comictalker.comictalker import ComicTalker logger = logging.getLogger("comictagger") +def group_for_plugin(plugin: Archiver | ComicTalker) -> str: + if isinstance(plugin, ComicTalker): + return f"Source {plugin.id}" + if isinstance(plugin, Archiver): + return "Archive" + raise NotImplementedError(f"Invalid plugin received: {plugin=}") + + def archiver(manager: settngs.Manager) -> None: for archiver in comicapi.comicarchive.archivers: if archiver.exe: @@ -26,27 +37,28 @@ def archiver(manager: settngs.Manager) -> None: def register_talker_settings(manager: settngs.Manager) -> None: - for talker_id, talker in comictaggerlib.ctsettings.talkers.items(): + for talker in comictaggerlib.ctsettings.talkers.values(): def api_options(manager: settngs.Manager) -> None: # The default needs to be unset or None. # This allows this setting to be unset with the empty string, allowing the default to change manager.add_setting( - f"--{talker_id}-key", + f"--{talker.id}-key", display_name="API Key", help=f"API Key for {talker.name} (default: {talker.default_api_key})", ) manager.add_setting( - f"--{talker_id}-url", + f"--{talker.id}-url", display_name="URL", help=f"URL for {talker.name} (default: {talker.default_api_url})", ) try: - manager.add_persistent_group("talker_" + talker_id, api_options, False) - manager.add_persistent_group("talker_" + talker_id, talker.register_settings, False) + manager.add_persistent_group(group_for_plugin(talker), api_options, False) + if hasattr(talker, "register_settings"): + manager.add_persistent_group(group_for_plugin(talker), talker.register_settings, False) except Exception: - logger.exception("Failed to register settings for %s", talker_id) + logger.exception("Failed to register settings for %s", talker.id) def validate_archive_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]: @@ -55,11 +67,11 @@ def validate_archive_settings(config: settngs.Config[ct_ns]) -> settngs.Config[c cfg = settngs.normalize_config(config, file=True, cmdline=True, default=False) for archiver in comicapi.comicarchive.archivers: exe_name = settngs.sanitize_name(archiver.exe) - if exe_name in cfg[0]["archiver"] and cfg[0]["archiver"][exe_name]: - if os.path.basename(cfg[0]["archiver"][exe_name]) == archiver.exe: - comicapi.utils.add_to_path(os.path.dirname(cfg[0]["archiver"][exe_name])) + if exe_name in cfg[0][group_for_plugin(archiver())] and cfg[0][group_for_plugin(archiver())][exe_name]: + if os.path.basename(cfg[0][group_for_plugin(archiver())][exe_name]) == archiver.exe: + comicapi.utils.add_to_path(os.path.dirname(cfg[0][group_for_plugin(archiver())][exe_name])) else: - archiver.exe = cfg[0]["archiver"][exe_name] + archiver.exe = cfg[0][group_for_plugin(archiver())][exe_name] return config @@ -67,12 +79,12 @@ def validate_archive_settings(config: settngs.Config[ct_ns]) -> settngs.Config[c def validate_talker_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct_ns]: # Apply talker settings from config file cfg = settngs.normalize_config(config, True, True) - for talker_id, talker in list(comictaggerlib.ctsettings.talkers.items()): + for talker in list(comictaggerlib.ctsettings.talkers.values()): try: - cfg[0]["talker_" + talker_id] = talker.parse_settings(cfg[0]["talker_" + talker_id]) + cfg[0][group_for_plugin(talker)] = talker.parse_settings(cfg[0][group_for_plugin(talker)]) except Exception as e: # Remove talker as we failed to apply the settings - del comictaggerlib.ctsettings.talkers[talker_id] + del comictaggerlib.ctsettings.talkers[talker.id] logger.exception("Failed to initialize talker settings: %s", e) return cast(settngs.Config[ct_ns], settngs.get_namespace(cfg, file=True, cmdline=True)) @@ -85,5 +97,5 @@ def validate_plugin_settings(config: settngs.Config[ct_ns]) -> settngs.Config[ct def register_plugin_settings(manager: settngs.Manager) -> None: - manager.add_persistent_group("archiver", archiver, False) + manager.add_persistent_group("Archive", archiver, False) register_talker_settings(manager) diff --git a/comictaggerlib/ctsettings/settngs_namespace.py b/comictaggerlib/ctsettings/settngs_namespace.py index 75df672..285b86a 100644 --- a/comictaggerlib/ctsettings/settngs_namespace.py +++ b/comictaggerlib/ctsettings/settngs_namespace.py @@ -8,40 +8,39 @@ import comictaggerlib.defaults class settngs_namespace(settngs.TypedNS): - commands_version: bool - commands_print: bool - commands_delete: bool - commands_copy: int - commands_save: bool - commands_rename: bool - commands_export_to_zip: bool - commands_only_set_cv_key: bool + Commands_version: bool + Commands_print: bool + Commands_delete: bool + Commands_copy: int + Commands_save: bool + Commands_rename: bool + Commands_export_to_zip: bool + Commands_only_set_cv_key: bool + Commands_list_plugins: bool - runtime_config: comictaggerlib.ctsettings.types.ComicTaggerPaths - runtime_verbose: int - runtime_abort_on_conflict: bool - runtime_delete_after_zip_export: bool - runtime_parse_filename: bool - runtime_issue_id: str - runtime_online: bool - runtime_metadata: comicapi.genericmetadata.GenericMetadata - runtime_interactive: bool - runtime_abort_on_low_confidence: bool - runtime_summary: bool - runtime_raw: bool - runtime_recursive: bool - runtime_script: str - runtime_split_words: bool - runtime_dryrun: bool - runtime_darkmode: bool - runtime_glob: bool - runtime_quiet: bool - runtime_type: list[int] - runtime_overwrite: bool - runtime_no_gui: bool - runtime_files: list[str] - - general_check_for_new_version: bool + Runtime_Options_config: comictaggerlib.ctsettings.types.ComicTaggerPaths + Runtime_Options_verbose: int + Runtime_Options_abort_on_conflict: bool + Runtime_Options_delete_after_zip_export: bool + Runtime_Options_parse_filename: bool + Runtime_Options_issue_id: str + Runtime_Options_online: bool + Runtime_Options_metadata: comicapi.genericmetadata.GenericMetadata + Runtime_Options_interactive: bool + Runtime_Options_abort_on_low_confidence: bool + Runtime_Options_summary: bool + Runtime_Options_raw: bool + Runtime_Options_recursive: bool + Runtime_Options_script: str + Runtime_Options_split_words: bool + Runtime_Options_dryrun: bool + Runtime_Options_darkmode: bool + Runtime_Options_glob: bool + Runtime_Options_quiet: bool + Runtime_Options_type: list[int] + Runtime_Options_overwrite: bool + Runtime_Options_no_gui: bool + Runtime_Options_files: list[str] internal_install_id: str internal_save_data_style: int @@ -56,50 +55,56 @@ class settngs_namespace(settngs.TypedNS): internal_sort_column: int internal_sort_direction: int - identifier_series_match_identify_thresh: int - identifier_border_crop_percent: int - identifier_publisher_filter: list[str] - identifier_series_match_search_thresh: int - identifier_clear_metadata_on_import: bool - identifier_auto_imprint: bool - identifier_sort_series_by_year: bool - identifier_exact_series_matches_first: bool - identifier_always_use_publisher_filter: bool - identifier_clear_form_before_populating: bool + Issue_Identifier_series_match_identify_thresh: int + Issue_Identifier_border_crop_percent: int + Issue_Identifier_publisher_filter: list[str] + Issue_Identifier_series_match_search_thresh: int + Issue_Identifier_clear_metadata_on_import: bool + Issue_Identifier_auto_imprint: bool + Issue_Identifier_sort_series_by_year: bool + Issue_Identifier_exact_series_matches_first: bool + Issue_Identifier_always_use_publisher_filter: bool + Issue_Identifier_clear_form_before_populating: bool - dialog_show_disclaimer: bool - dialog_dont_notify_about_this_version: str - dialog_ask_about_usage_stats: bool + Filename_Parsing_complicated_parser: bool + Filename_Parsing_remove_c2c: bool + Filename_Parsing_remove_fcbd: bool + Filename_Parsing_remove_publisher: bool - filename_complicated_parser: bool - filename_remove_c2c: bool - filename_remove_fcbd: bool - filename_remove_publisher: bool + Sources_source: str + Sources_remove_html_tables: bool - talker_source: str - talker_remove_html_tables: bool + Comic_Book_Lover_assume_lone_credit_is_primary: bool + Comic_Book_Lover_copy_characters_to_tags: bool + Comic_Book_Lover_copy_teams_to_tags: bool + Comic_Book_Lover_copy_locations_to_tags: bool + Comic_Book_Lover_copy_storyarcs_to_tags: bool + Comic_Book_Lover_copy_notes_to_comments: bool + Comic_Book_Lover_copy_weblink_to_comments: bool + Comic_Book_Lover_apply_transform_on_import: bool + Comic_Book_Lover_apply_transform_on_bulk_operation: bool - cbl_assume_lone_credit_is_primary: bool - cbl_copy_characters_to_tags: bool - cbl_copy_teams_to_tags: bool - cbl_copy_locations_to_tags: bool - cbl_copy_storyarcs_to_tags: bool - cbl_copy_notes_to_comments: bool - cbl_copy_weblink_to_comments: bool - cbl_apply_transform_on_import: bool - cbl_apply_transform_on_bulk_operation: bool + File_Rename_template: str + File_Rename_issue_number_padding: int + File_Rename_use_smart_string_cleanup: bool + File_Rename_set_extension_based_on_archive: bool + File_Rename_dir: str + File_Rename_move_to_dir: bool + File_Rename_strict: bool + File_Rename_replacements: comictaggerlib.defaults.Replacements - rename_template: str - rename_issue_number_padding: int - rename_use_smart_string_cleanup: bool - rename_set_extension_based_on_archive: bool - rename_dir: str - rename_move_to_dir: bool - rename_strict: bool - rename_replacements: comictaggerlib.defaults.Replacements + Auto_Tag_save_on_low_confidence: bool + Auto_Tag_dont_use_year_when_identifying: bool + Auto_Tag_assume_1_if_no_issue_num: bool + Auto_Tag_ignore_leading_numbers_in_filename: bool + Auto_Tag_remove_archive_after_successful_match: bool - autotag_save_on_low_confidence: bool - autotag_dont_use_year_when_identifying: bool - autotag_assume_1_if_no_issue_num: bool - autotag_ignore_leading_numbers_in_filename: bool - autotag_remove_archive_after_successful_match: bool + General_check_for_new_version: bool + + Dialog_Flags_show_disclaimer: bool + Dialog_Flags_dont_notify_about_this_version: str + Dialog_Flags_ask_about_usage_stats: bool + + Source_comicvine_comicvine_key: str + Source_comicvine_comicvine_url: str + Source_comicvine_cv_use_series_start_as_volume: bool diff --git a/comictaggerlib/gui.py b/comictaggerlib/gui.py index 9c77ba5..b51a4d5 100644 --- a/comictaggerlib/gui.py +++ b/comictaggerlib/gui.py @@ -96,7 +96,7 @@ def open_tagger_window( ) -> None: os.environ["QtWidgets.QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" args = [] - if config[0].runtime_darkmode: + if config[0].Runtime_Options_darkmode: args.extend(["-platform", "windows:darkmode=2"]) args.extend(sys.argv) app = Application(args) @@ -106,7 +106,7 @@ def open_tagger_window( raise SystemExit(1) # needed to catch initial open file events (macOS) - app.openFileRequest.connect(lambda x: config[0].runtime_files.append(x.toLocalFile())) + app.openFileRequest.connect(lambda x: config[0].Runtime_Options_files.append(x.toLocalFile())) if platform.system() == "Darwin": # Set the MacOS dock icon @@ -134,7 +134,7 @@ def open_tagger_window( QtWidgets.QApplication.processEvents() try: - tagger_window = TaggerWindow(config[0].runtime_files, config, talkers) + tagger_window = TaggerWindow(config[0].Runtime_Options_files, config, talkers) tagger_window.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png"))) tagger_window.show() diff --git a/comictaggerlib/issueidentifier.py b/comictaggerlib/issueidentifier.py index 6e9f5b2..d8ac1de 100644 --- a/comictaggerlib/issueidentifier.py +++ b/comictaggerlib/issueidentifier.py @@ -96,10 +96,10 @@ class IssueIdentifier: # used to eliminate series names that are too long based on our search # string - self.series_match_thresh = config.identifier_series_match_identify_thresh + self.series_match_thresh = config.Issue_Identifier_series_match_identify_thresh # used to eliminate unlikely publishers - self.publisher_filter = [s.strip().casefold() for s in config.identifier_publisher_filter] + self.publisher_filter = [s.strip().casefold() for s in config.Issue_Identifier_publisher_filter] self.additional_metadata = GenericMetadata() self.output_function: Callable[[str], None] = IssueIdentifier.default_write_output @@ -239,10 +239,10 @@ class IssueIdentifier: # try to get some metadata from filename md_from_filename = ca.metadata_from_filename( - self.config.filename_complicated_parser, - self.config.filename_remove_c2c, - self.config.filename_remove_fcbd, - self.config.filename_remove_publisher, + self.config.Filename_Parsing_complicated_parser, + self.config.Filename_Parsing_remove_c2c, + self.config.Filename_Parsing_remove_fcbd, + self.config.Filename_Parsing_remove_publisher, ) working_md = md_from_filename.copy() @@ -291,7 +291,7 @@ class IssueIdentifier: return Score(score=0, url="", hash=0) try: - url_image_data = ImageFetcher(self.config.runtime_config.user_cache_dir).fetch( + url_image_data = ImageFetcher(self.config.Runtime_Options_config.user_cache_dir).fetch( primary_img_url, blocking=True ) except ImageFetcherException as e: @@ -313,7 +313,7 @@ class IssueIdentifier: if use_remote_alternates: for alt_url in alt_urls: try: - alt_url_image_data = ImageFetcher(self.config.runtime_config.user_cache_dir).fetch( + alt_url_image_data = ImageFetcher(self.config.Runtime_Options_config.user_cache_dir).fetch( alt_url, blocking=True ) except ImageFetcherException as e: @@ -499,7 +499,7 @@ class IssueIdentifier: if narrow_cover_hash is not None: hash_list.append(narrow_cover_hash) - cropped_border = self.crop_border(cover_image_data, self.config.identifier_border_crop_percent) + cropped_border = self.crop_border(cover_image_data, self.config.Issue_Identifier_border_crop_percent) if cropped_border is not None: hash_list.append(self.calculate_hash(cropped_border)) logger.info("Adding cropped cover to the hashlist") diff --git a/comictaggerlib/issueselectionwindow.py b/comictaggerlib/issueselectionwindow.py index baabc10..82d57e5 100644 --- a/comictaggerlib/issueselectionwindow.py +++ b/comictaggerlib/issueselectionwindow.py @@ -52,7 +52,10 @@ class IssueSelectionWindow(QtWidgets.QDialog): uic.loadUi(ui_path / "issueselectionwindow.ui", self) self.coverWidget = CoverImageWidget( - self.coverImageContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker + self.coverImageContainer, + CoverImageWidget.AltCoverMode, + config.Runtime_Options_config.user_cache_dir, + talker, ) gridlayout = QtWidgets.QGridLayout(self.coverImageContainer) gridlayout.addWidget(self.coverWidget) @@ -95,7 +98,7 @@ class IssueSelectionWindow(QtWidgets.QDialog): self.imageIssuesSourceWidget = CoverImageWidget( self.imageIssuesSourceLogo, CoverImageWidget.URLMode, - config.runtime_config.user_cache_dir, + config.Runtime_Options_config.user_cache_dir, talker, False, ) diff --git a/comictaggerlib/main.py b/comictaggerlib/main.py index 67b1742..8dbe91b 100644 --- a/comictaggerlib/main.py +++ b/comictaggerlib/main.py @@ -91,7 +91,7 @@ def configure_locale() -> None: def update_publishers(config: settngs.Config[ct_ns]) -> None: - json_file = config[0].runtime_config.user_config_dir / "publishers.json" + json_file = config[0].Runtime_Options_config.user_config_dir / "publishers.json" if json_file.exists(): try: comicapi.utils.update_publishers(json.loads(json_file.read_text("utf-8"))) @@ -121,6 +121,18 @@ class App: comicapi.comicarchive.load_archive_plugins() ctsettings.talkers = comictalker.get_talkers(version, opts.config.user_cache_dir) + def list_plugins( + self, talkers: list[comictalker.ComicTalker], archivers: list[type[comicapi.comicarchive.Archiver]] + ) -> None: + print("Metadata Sources: (ID: Name URL)") # noqa: T201 + for talker in talkers: + print(f"{talker.id}: {talker.name} {talker.default_api_url}") # noqa: T201 + + print("\nComic Archive: (Name: extension, exe)") # noqa: T201 + for archiver in archivers: + a = archiver() + print(f"{a.name()}: {a.extension()}, {a.exe}") # noqa: T201 + def initialize(self) -> argparse.Namespace: conf, _ = self.initial_arg_parser.parse_known_args() assert conf is not None @@ -141,7 +153,7 @@ class App: config_paths.user_config_dir / "settings.json", list(args) or None ) config = cast(settngs.Config[ct_ns], self.manager.get_namespace(cfg, file=True, cmdline=True)) - config[0].runtime_config = config_paths + config[0].Runtime_Options_config = config_paths config = ctsettings.validate_commandline_settings(config, self.manager) config = ctsettings.validate_file_settings(config) @@ -170,7 +182,7 @@ class App: if len(talkers) < 1: error = error = ( - f"Failed to load any talkers, please re-install and check the log located in '{self.config[0].runtime_config.user_log_dir}' for more details", + f"Failed to load any talkers, please re-install and check the log located in '{self.config[0].Runtime_Options_config.user_log_dir}' for more details", True, ) @@ -183,34 +195,38 @@ class App: comicapi.utils.load_publishers() update_publishers(self.config) + if self.config[0].Commands_list_plugins: + self.list_plugins(list(talkers.values()), comicapi.comicarchive.archivers) + return + # manage the CV API key # None comparison is used so that the empty string can unset the value if not error and ( - self.config[0].talker_comicvine_comicvine_key is not None # type: ignore[attr-defined] - or self.config[0].talker_comicvine_comicvine_url is not None # type: ignore[attr-defined] + self.config[0].Source_comicvine_comicvine_key is not None + or self.config[0].Source_comicvine_comicvine_url is not None ): - settings_path = self.config[0].runtime_config.user_config_dir / "settings.json" + settings_path = self.config[0].Runtime_Options_config.user_config_dir / "settings.json" if self.config_load_success: self.manager.save_file(self.config[0], settings_path) - if self.config[0].commands_only_set_cv_key: + if self.config[0].Commands_only_set_cv_key: if self.config_load_success: print("Key set") # noqa: T201 return if not self.config_load_success: error = ( - f"Failed to load settings, check the log located in '{self.config[0].runtime_config.user_log_dir}' for more details", + f"Failed to load settings, check the log located in '{self.config[0].Runtime_Options_config.user_log_dir}' for more details", True, ) - if not self.config[0].runtime_no_gui: + if not self.config[0].Runtime_Options_no_gui: try: from comictaggerlib import gui return gui.open_tagger_window(talkers, self.config, error) except ImportError: - self.config[0].runtime_no_gui = True + self.config[0].Runtime_Options_no_gui = True logger.warning("PyQt5 is not available. ComicTagger is limited to command-line mode.") # GUI mode is not available or CLI mode was requested diff --git a/comictaggerlib/matchselectionwindow.py b/comictaggerlib/matchselectionwindow.py index 0558195..1e6046a 100644 --- a/comictaggerlib/matchselectionwindow.py +++ b/comictaggerlib/matchselectionwindow.py @@ -45,7 +45,7 @@ class MatchSelectionWindow(QtWidgets.QDialog): uic.loadUi(ui_path / "matchselectionwindow.ui", self) self.altCoverWidget = CoverImageWidget( - self.altCoverContainer, CoverImageWidget.AltCoverMode, config.runtime_config.user_cache_dir, talker + self.altCoverContainer, CoverImageWidget.AltCoverMode, config.Runtime_Options_config.user_cache_dir, talker ) gridlayout = QtWidgets.QGridLayout(self.altCoverContainer) gridlayout.addWidget(self.altCoverWidget) diff --git a/comictaggerlib/renamewindow.py b/comictaggerlib/renamewindow.py index a74831d..608c9f9 100644 --- a/comictaggerlib/renamewindow.py +++ b/comictaggerlib/renamewindow.py @@ -62,32 +62,32 @@ class RenameWindow(QtWidgets.QDialog): self.rename_list: list[str] = [] self.btnSettings.clicked.connect(self.modify_settings) - platform = "universal" if self.config[0].rename_strict else "auto" - self.renamer = FileRenamer(None, platform=platform, replacements=self.config[0].rename_replacements) + platform = "universal" if self.config[0].File_Rename_strict else "auto" + self.renamer = FileRenamer(None, platform=platform, replacements=self.config[0].File_Rename_replacements) self.do_preview() def config_renamer(self, ca: ComicArchive, md: GenericMetadata | None = None) -> str: - self.renamer.set_template(self.config[0].rename_template) - self.renamer.set_issue_zero_padding(self.config[0].rename_issue_number_padding) - self.renamer.set_smart_cleanup(self.config[0].rename_use_smart_string_cleanup) - self.renamer.replacements = self.config[0].rename_replacements + self.renamer.set_template(self.config[0].File_Rename_template) + self.renamer.set_issue_zero_padding(self.config[0].File_Rename_issue_number_padding) + self.renamer.set_smart_cleanup(self.config[0].File_Rename_use_smart_string_cleanup) + self.renamer.replacements = self.config[0].File_Rename_replacements new_ext = ca.path.suffix # default - if self.config[0].rename_set_extension_based_on_archive: + if self.config[0].File_Rename_set_extension_based_on_archive: new_ext = ca.extension() if md is None: md = ca.read_metadata(self.data_style) if md.is_empty: md = ca.metadata_from_filename( - self.config[0].filename_complicated_parser, - self.config[0].filename_remove_c2c, - self.config[0].filename_remove_fcbd, - self.config[0].filename_remove_publisher, + self.config[0].Filename_Parsing_complicated_parser, + self.config[0].Filename_Parsing_remove_c2c, + self.config[0].Filename_Parsing_remove_fcbd, + self.config[0].Filename_Parsing_remove_publisher, ) self.renamer.set_metadata(md) - self.renamer.move = self.config[0].rename_move_to_dir + self.renamer.move = self.config[0].File_Rename_move_to_dir return new_ext def do_preview(self) -> None: @@ -100,7 +100,7 @@ class RenameWindow(QtWidgets.QDialog): try: new_name = self.renamer.determine_name(new_ext) except ValueError as e: - logger.exception("Invalid format string: %s", self.config[0].rename_template) + logger.exception("Invalid format string: %s", self.config[0].File_Rename_template) QtWidgets.QMessageBox.critical( self, "Invalid format string!", @@ -114,7 +114,7 @@ class RenameWindow(QtWidgets.QDialog): return except Exception as e: logger.exception( - "Formatter failure: %s metadata: %s", self.config[0].rename_template, self.renamer.metadata + "Formatter failure: %s metadata: %s", self.config[0].File_Rename_template, self.renamer.metadata ) QtWidgets.QMessageBox.critical( self, @@ -190,7 +190,7 @@ class RenameWindow(QtWidgets.QDialog): folder = get_rename_dir( comic[0], - self.config[0].rename_dir if self.config[0].rename_move_to_dir else None, + self.config[0].File_Rename_dir if self.config[0].File_Rename_move_to_dir else None, ) full_path = folder / comic[1] diff --git a/comictaggerlib/seriesselectionwindow.py b/comictaggerlib/seriesselectionwindow.py index 2f3e282..9b5d661 100644 --- a/comictaggerlib/seriesselectionwindow.py +++ b/comictaggerlib/seriesselectionwindow.py @@ -116,7 +116,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): uic.loadUi(ui_path / "seriesselectionwindow.ui", self) self.imageWidget = CoverImageWidget( - self.imageContainer, CoverImageWidget.URLMode, config.runtime_config.user_cache_dir, talker + self.imageContainer, CoverImageWidget.URLMode, config.Runtime_Options_config.user_cache_dir, talker ) gridlayout = QtWidgets.QGridLayout(self.imageContainer) gridlayout.addWidget(self.imageWidget) @@ -161,7 +161,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.progdialog: QtWidgets.QProgressDialog | None = None self.search_thread: SearchThread | None = None - self.use_filter = self.config.identifier_always_use_publisher_filter + self.use_filter = self.config.Issue_Identifier_always_use_publisher_filter # Load to retrieve settings self.talker = talker @@ -172,7 +172,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): self.imageSourceWidget = CoverImageWidget( self.imageSourceLogo, CoverImageWidget.URLMode, - config.runtime_config.user_cache_dir, + config.Runtime_Options_config.user_cache_dir, talker, False, ) @@ -356,7 +356,11 @@ class SeriesSelectionWindow(QtWidgets.QDialog): def perform_query(self, refresh: bool = False) -> None: self.search_thread = SearchThread( - self.talker, self.series_name, refresh, self.literal, self.config.identifier_series_match_search_thresh + self.talker, + self.series_name, + refresh, + self.literal, + self.config.Issue_Identifier_series_match_search_thresh, ) self.search_thread.searchComplete.connect(self.search_complete) self.search_thread.progressUpdate.connect(self.search_progress_update) @@ -409,7 +413,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): # filter the publishers if enabled set if self.use_filter: try: - publisher_filter = {s.strip().casefold() for s in self.config.identifier_publisher_filter} + publisher_filter = {s.strip().casefold() for s in self.config.Issue_Identifier_publisher_filter} # use '' as publisher name if None self.series_list = dict( filter( @@ -425,7 +429,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): # compare as str in case extra chars ie. '1976?' # - missing (none) values being converted to 'None' - consistent with prior behaviour in v1.2.3 # sort by start_year if set - if self.config.identifier_sort_series_by_year: + if self.config.Issue_Identifier_sort_series_by_year: try: self.series_list = dict( natsort.natsorted( @@ -445,7 +449,7 @@ class SeriesSelectionWindow(QtWidgets.QDialog): logger.exception("bad data error sorting results by count_of_issues") # move sanitized matches to the front - if self.config.identifier_exact_series_matches_first: + if self.config.Issue_Identifier_exact_series_matches_first: try: sanitized = utils.sanitize_title(self.series_name, False).casefold() sanitized_no_articles = utils.sanitize_title(self.series_name, True).casefold() diff --git a/comictaggerlib/settingswindow.py b/comictaggerlib/settingswindow.py index ff02ff7..7187bf0 100644 --- a/comictaggerlib/settingswindow.py +++ b/comictaggerlib/settingswindow.py @@ -188,7 +188,7 @@ class SettingsWindow(QtWidgets.QDialog): self.leRenameTemplate.setToolTip(f"
{html.escape(template_tooltip)}
") self.rename_error: Exception | None = None - self.sources: dict = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs( + self.sources = comictaggerlib.ui.talkeruigenerator.generate_source_option_tabs( self.tComicTalkers, self.config, self.talkers ) self.connect_signals() @@ -307,43 +307,45 @@ class SettingsWindow(QtWidgets.QDialog): self.leRarExePath.setText(getattr(self.config[0], self.config[1]["archiver"].v["rar"].internal_name)) else: self.leRarExePath.setEnabled(False) - self.sbNameMatchIdentifyThresh.setValue(self.config[0].identifier_series_match_identify_thresh) - self.sbNameMatchSearchThresh.setValue(self.config[0].identifier_series_match_search_thresh) - self.tePublisherFilter.setPlainText("\n".join(self.config[0].identifier_publisher_filter)) + self.sbNameMatchIdentifyThresh.setValue(self.config[0].Issue_Identifier_series_match_identify_thresh) + self.sbNameMatchSearchThresh.setValue(self.config[0].Issue_Identifier_series_match_search_thresh) + self.tePublisherFilter.setPlainText("\n".join(self.config[0].Issue_Identifier_publisher_filter)) - self.cbxCheckForNewVersion.setChecked(self.config[0].general_check_for_new_version) + self.cbxCheckForNewVersion.setChecked(self.config[0].General_check_for_new_version) - self.cbxComplicatedParser.setChecked(self.config[0].filename_complicated_parser) - self.cbxRemoveC2C.setChecked(self.config[0].filename_remove_c2c) - self.cbxRemoveFCBD.setChecked(self.config[0].filename_remove_fcbd) - self.cbxRemovePublisher.setChecked(self.config[0].filename_remove_publisher) + self.cbxComplicatedParser.setChecked(self.config[0].Filename_Parsing_complicated_parser) + self.cbxRemoveC2C.setChecked(self.config[0].Filename_Parsing_remove_c2c) + self.cbxRemoveFCBD.setChecked(self.config[0].Filename_Parsing_remove_fcbd) + self.cbxRemovePublisher.setChecked(self.config[0].Filename_Parsing_remove_publisher) self.switch_parser() - self.cbxClearFormBeforePopulating.setChecked(self.config[0].identifier_clear_form_before_populating) - self.cbxUseFilter.setChecked(self.config[0].identifier_always_use_publisher_filter) - self.cbxSortByYear.setChecked(self.config[0].identifier_sort_series_by_year) - self.cbxExactMatches.setChecked(self.config[0].identifier_exact_series_matches_first) + self.cbxClearFormBeforePopulating.setChecked(self.config[0].Issue_Identifier_clear_form_before_populating) + self.cbxUseFilter.setChecked(self.config[0].Issue_Identifier_always_use_publisher_filter) + self.cbxSortByYear.setChecked(self.config[0].Issue_Identifier_sort_series_by_year) + self.cbxExactMatches.setChecked(self.config[0].Issue_Identifier_exact_series_matches_first) - self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].cbl_assume_lone_credit_is_primary) - self.cbxCopyCharactersToTags.setChecked(self.config[0].cbl_copy_characters_to_tags) - self.cbxCopyTeamsToTags.setChecked(self.config[0].cbl_copy_teams_to_tags) - self.cbxCopyLocationsToTags.setChecked(self.config[0].cbl_copy_locations_to_tags) - self.cbxCopyStoryArcsToTags.setChecked(self.config[0].cbl_copy_storyarcs_to_tags) - self.cbxCopyNotesToComments.setChecked(self.config[0].cbl_copy_notes_to_comments) - self.cbxCopyWebLinkToComments.setChecked(self.config[0].cbl_copy_weblink_to_comments) - self.cbxApplyCBLTransformOnCVIMport.setChecked(self.config[0].cbl_apply_transform_on_import) - self.cbxApplyCBLTransformOnBatchOperation.setChecked(self.config[0].cbl_apply_transform_on_bulk_operation) + self.cbxAssumeLoneCreditIsPrimary.setChecked(self.config[0].Comic_Book_Lover_assume_lone_credit_is_primary) + self.cbxCopyCharactersToTags.setChecked(self.config[0].Comic_Book_Lover_copy_characters_to_tags) + self.cbxCopyTeamsToTags.setChecked(self.config[0].Comic_Book_Lover_copy_teams_to_tags) + self.cbxCopyLocationsToTags.setChecked(self.config[0].Comic_Book_Lover_copy_locations_to_tags) + self.cbxCopyStoryArcsToTags.setChecked(self.config[0].Comic_Book_Lover_copy_storyarcs_to_tags) + self.cbxCopyNotesToComments.setChecked(self.config[0].Comic_Book_Lover_copy_notes_to_comments) + self.cbxCopyWebLinkToComments.setChecked(self.config[0].Comic_Book_Lover_copy_weblink_to_comments) + self.cbxApplyCBLTransformOnCVIMport.setChecked(self.config[0].Comic_Book_Lover_apply_transform_on_import) + self.cbxApplyCBLTransformOnBatchOperation.setChecked( + self.config[0].Comic_Book_Lover_apply_transform_on_bulk_operation + ) - self.leRenameTemplate.setText(self.config[0].rename_template) - self.leIssueNumPadding.setText(str(self.config[0].rename_issue_number_padding)) - self.cbxSmartCleanup.setChecked(self.config[0].rename_use_smart_string_cleanup) - self.cbxChangeExtension.setChecked(self.config[0].rename_set_extension_based_on_archive) - self.cbxMoveFiles.setChecked(self.config[0].rename_move_to_dir) - self.leDirectory.setText(self.config[0].rename_dir) - self.cbxRenameStrict.setChecked(self.config[0].rename_strict) + self.leRenameTemplate.setText(self.config[0].File_Rename_template) + self.leIssueNumPadding.setText(str(self.config[0].File_Rename_issue_number_padding)) + self.cbxSmartCleanup.setChecked(self.config[0].File_Rename_use_smart_string_cleanup) + self.cbxChangeExtension.setChecked(self.config[0].File_Rename_set_extension_based_on_archive) + self.cbxMoveFiles.setChecked(self.config[0].File_Rename_move_to_dir) + self.leDirectory.setText(self.config[0].File_Rename_dir) + self.cbxRenameStrict.setChecked(self.config[0].File_Rename_strict) for table, replacments in zip( - (self.twLiteralReplacements, self.twValueReplacements), self.config[0].rename_replacements + (self.twLiteralReplacements, self.twValueReplacements), self.config[0].File_Rename_replacements ): table.clearContents() for i in reversed(range(table.rowCount())): @@ -383,7 +385,7 @@ class SettingsWindow(QtWidgets.QDialog): self.rename_test() if self.rename_error is not None: if isinstance(self.rename_error, ValueError): - logger.exception("Invalid format string: %s", self.config[0].rename_template) + logger.exception("Invalid format string: %s", self.config[0].File_Rename_template) QtWidgets.QMessageBox.critical( self, "Invalid format string!", @@ -397,7 +399,7 @@ class SettingsWindow(QtWidgets.QDialog): return else: logger.exception( - "Formatter failure: %s metadata: %s", self.config[0].rename_template, self.renamer.metadata + "Formatter failure: %s metadata: %s", self.config[0].File_Rename_template, self.renamer.metadata ) QtWidgets.QMessageBox.critical( self, @@ -420,48 +422,50 @@ class SettingsWindow(QtWidgets.QDialog): if not str(self.leIssueNumPadding.text()).isdigit(): self.leIssueNumPadding.setText("0") - self.config[0].general_check_for_new_version = self.cbxCheckForNewVersion.isChecked() + self.config[0].General_check_for_new_version = self.cbxCheckForNewVersion.isChecked() - self.config[0].identifier_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value() - self.config[0].identifier_series_match_search_thresh = self.sbNameMatchSearchThresh.value() - self.config[0].identifier_publisher_filter = utils.split(self.tePublisherFilter.toPlainText(), "\n") + self.config[0].Issue_Identifier_series_match_identify_thresh = self.sbNameMatchIdentifyThresh.value() + self.config[0].Issue_Identifier_series_match_search_thresh = self.sbNameMatchSearchThresh.value() + self.config[0].Issue_Identifier_publisher_filter = utils.split(self.tePublisherFilter.toPlainText(), "\n") - self.config[0].filename_complicated_parser = self.cbxComplicatedParser.isChecked() - self.config[0].filename_remove_c2c = self.cbxRemoveC2C.isChecked() - self.config[0].filename_remove_fcbd = self.cbxRemoveFCBD.isChecked() - self.config[0].filename_remove_publisher = self.cbxRemovePublisher.isChecked() + self.config[0].Filename_Parsing_complicated_parser = self.cbxComplicatedParser.isChecked() + self.config[0].Filename_Parsing_remove_c2c = self.cbxRemoveC2C.isChecked() + self.config[0].Filename_Parsing_remove_fcbd = self.cbxRemoveFCBD.isChecked() + self.config[0].Filename_Parsing_remove_publisher = self.cbxRemovePublisher.isChecked() - self.config[0].identifier_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked() - self.config[0].identifier_always_use_publisher_filter = self.cbxUseFilter.isChecked() - self.config[0].identifier_sort_series_by_year = self.cbxSortByYear.isChecked() - self.config[0].identifier_exact_series_matches_first = self.cbxExactMatches.isChecked() + self.config[0].Issue_Identifier_clear_form_before_populating = self.cbxClearFormBeforePopulating.isChecked() + self.config[0].Issue_Identifier_always_use_publisher_filter = self.cbxUseFilter.isChecked() + self.config[0].Issue_Identifier_sort_series_by_year = self.cbxSortByYear.isChecked() + self.config[0].Issue_Identifier_exact_series_matches_first = self.cbxExactMatches.isChecked() - self.config[0].cbl_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked() - self.config[0].cbl_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked() - self.config[0].cbl_copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked() - self.config[0].cbl_copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked() - self.config[0].cbl_copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked() - self.config[0].cbl_copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked() - self.config[0].cbl_copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked() - self.config[0].cbl_apply_transform_on_import = self.cbxApplyCBLTransformOnCVIMport.isChecked() - self.config[0].cbl_apply_transform_on_bulk_operation = self.cbxApplyCBLTransformOnBatchOperation.isChecked() + self.config[0].Comic_Book_Lover_assume_lone_credit_is_primary = self.cbxAssumeLoneCreditIsPrimary.isChecked() + self.config[0].Comic_Book_Lover_copy_characters_to_tags = self.cbxCopyCharactersToTags.isChecked() + self.config[0].Comic_Book_Lover_copy_teams_to_tags = self.cbxCopyTeamsToTags.isChecked() + self.config[0].Comic_Book_Lover_copy_locations_to_tags = self.cbxCopyLocationsToTags.isChecked() + self.config[0].Comic_Book_Lover_copy_storyarcs_to_tags = self.cbxCopyStoryArcsToTags.isChecked() + self.config[0].Comic_Book_Lover_copy_notes_to_comments = self.cbxCopyNotesToComments.isChecked() + self.config[0].Comic_Book_Lover_copy_weblink_to_comments = self.cbxCopyWebLinkToComments.isChecked() + self.config[0].Comic_Book_Lover_apply_transform_on_import = self.cbxApplyCBLTransformOnCVIMport.isChecked() + self.config.values.Comic_Book_Lover_apply_transform_on_bulk_operation = ( + self.cbxApplyCBLTransformOnBatchOperation.isChecked() + ) - self.config[0].rename_template = str(self.leRenameTemplate.text()) - self.config[0].rename_issue_number_padding = int(self.leIssueNumPadding.text()) - self.config[0].rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked() - self.config[0].rename_set_extension_based_on_archive = self.cbxChangeExtension.isChecked() - self.config[0].rename_move_to_dir = self.cbxMoveFiles.isChecked() - self.config[0].rename_dir = self.leDirectory.text() + self.config[0].File_Rename_template = str(self.leRenameTemplate.text()) + self.config[0].File_Rename_issue_number_padding = int(self.leIssueNumPadding.text()) + self.config[0].File_Rename_use_smart_string_cleanup = self.cbxSmartCleanup.isChecked() + self.config[0].File_Rename_set_extension_based_on_archive = self.cbxChangeExtension.isChecked() + self.config[0].File_Rename_move_to_dir = self.cbxMoveFiles.isChecked() + self.config[0].File_Rename_dir = self.leDirectory.text() - self.config[0].rename_strict = self.cbxRenameStrict.isChecked() - self.config[0].rename_replacements = self.get_replacements() + self.config[0].File_Rename_strict = self.cbxRenameStrict.isChecked() + self.config[0].File_Rename_replacements = self.get_replacements() # Read settings from talker tabs comictaggerlib.ui.talkeruigenerator.form_settings_to_config(self.sources, self.config) self.update_talkers_config() - settngs.save_file(self.config, self.config[0].runtime_config.user_config_dir / "settings.json") + settngs.save_file(self.config, self.config[0].Runtime_Options_config.user_config_dir / "settings.json") self.parent().config = self.config QtWidgets.QDialog.accept(self) @@ -474,8 +478,8 @@ class SettingsWindow(QtWidgets.QDialog): self.select_file(self.leRarExePath, "RAR") def clear_cache(self) -> None: - ImageFetcher(self.config[0].runtime_config.user_cache_dir).clear_cache() - ComicCacher(self.config[0].runtime_config.user_cache_dir, version).clear_cache() + ImageFetcher(self.config[0].Runtime_Options_config.user_cache_dir).clear_cache() + ComicCacher(self.config[0].Runtime_Options_config.user_cache_dir, version).clear_cache() QtWidgets.QMessageBox.information(self, self.name, "Cache has been cleared.") def reset_settings(self) -> None: diff --git a/comictaggerlib/taggerwindow.py b/comictaggerlib/taggerwindow.py index 9deb6f5..e935119 100644 --- a/comictaggerlib/taggerwindow.py +++ b/comictaggerlib/taggerwindow.py @@ -156,10 +156,10 @@ class TaggerWindow(QtWidgets.QMainWindow): self.setWindowIcon(QtGui.QIcon(str(graphics_path / "app.png"))) - if config[0].runtime_type and isinstance(config[0].runtime_type[0], int): + if config[0].Runtime_Options_type and isinstance(config[0].Runtime_Options_type[0], int): # respect the command line option tag type - config[0].internal_save_data_style = config[0].runtime_type[0] - config[0].internal_load_data_style = config[0].runtime_type[0] + config[0].internal_save_data_style = config[0].Runtime_Options_type[0] + config[0].internal_load_data_style = config[0].Runtime_Options_type[0] self.save_data_style = config[0].internal_save_data_style self.load_data_style = config[0].internal_load_data_style @@ -245,7 +245,7 @@ class TaggerWindow(QtWidgets.QMainWindow): if len(file_list) != 0: self.fileSelectionList.add_path_list(file_list) - if self.config[0].dialog_show_disclaimer: + if self.config[0].Dialog_Flags_show_disclaimer: checked = OptionalMessageDialog.msg( self, "Welcome!", @@ -264,15 +264,15 @@ class TaggerWindow(QtWidgets.QMainWindow): Have fun! """, ) - self.config[0].dialog_show_disclaimer = not checked + self.config[0].Dialog_Flags_show_disclaimer = not checked - if self.config[0].general_check_for_new_version: + if self.config[0].General_check_for_new_version: self.check_latest_version_online() def current_talker(self) -> ComicTalker: - if self.config[0].talker_source in self.talkers: - return self.talkers[self.config[0].talker_source] - logger.error("Could not find the '%s' talker", self.config[0].talker_source) + if self.config[0].Sources_source in self.talkers: + return self.talkers[self.config[0].Sources_source] + logger.error("Could not find the '%s' talker", self.config[0].Sources_source) raise SystemExit(2) def open_file_event(self, url: QtCore.QUrl) -> None: @@ -285,7 +285,7 @@ class TaggerWindow(QtWidgets.QMainWindow): def setup_logger(self) -> ApplicationLogWindow: try: - current_logs = (self.config[0].runtime_config.user_log_dir / "ComicTagger.log").read_text("utf-8") + current_logs = (self.config[0].Runtime_Options_config.user_log_dir / "ComicTagger.log").read_text("utf-8") except Exception: current_logs = "" root_logger = logging.getLogger() @@ -618,10 +618,10 @@ class TaggerWindow(QtWidgets.QMainWindow): def actual_load_current_archive(self) -> None: if self.metadata.is_empty and self.comic_archive is not None: self.metadata = self.comic_archive.metadata_from_filename( - self.config[0].filename_complicated_parser, - self.config[0].filename_remove_c2c, - self.config[0].filename_remove_fcbd, - self.config[0].filename_remove_publisher, + self.config[0].Filename_Parsing_complicated_parser, + self.config[0].Filename_Parsing_remove_c2c, + self.config[0].Filename_Parsing_remove_fcbd, + self.config[0].Filename_Parsing_remove_publisher, ) if len(self.metadata.pages) == 0 and self.comic_archive is not None: self.metadata.set_default_page_list(self.comic_archive.get_number_of_pages()) @@ -967,10 +967,10 @@ class TaggerWindow(QtWidgets.QMainWindow): # copy the form onto metadata object self.form_to_metadata() new_metadata = self.comic_archive.metadata_from_filename( - self.config[0].filename_complicated_parser, - self.config[0].filename_remove_c2c, - self.config[0].filename_remove_fcbd, - self.config[0].filename_remove_publisher, + self.config[0].Filename_Parsing_complicated_parser, + self.config[0].Filename_Parsing_remove_c2c, + self.config[0].Filename_Parsing_remove_fcbd, + self.config[0].Filename_Parsing_remove_publisher, split_words, ) if new_metadata is not None: @@ -1079,10 +1079,10 @@ class TaggerWindow(QtWidgets.QMainWindow): else: QtWidgets.QApplication.restoreOverrideCursor() if new_metadata is not None: - if self.config[0].cbl_apply_transform_on_import: + if self.config[0].Comic_Book_Lover_apply_transform_on_import: new_metadata = CBLTransformer(new_metadata, self.config[0]).apply() - if self.config[0].identifier_clear_form_before_populating: + if self.config[0].Issue_Identifier_clear_form_before_populating: self.clear_form() notes = ( @@ -1093,7 +1093,7 @@ class TaggerWindow(QtWidgets.QMainWindow): new_metadata.replace( notes=utils.combine_notes(self.metadata.notes, notes, "Tagged with ComicTagger"), description=cleanup_html( - new_metadata.description, self.config[0].talker_remove_html_tables + new_metadata.description, self.config[0].Sources_remove_html_tables ), ) ) @@ -1636,7 +1636,10 @@ class TaggerWindow(QtWidgets.QMainWindow): if ca.has_metadata(src_style) and ca.is_writable(): md = ca.read_metadata(src_style) - if dest_style == MetaDataStyle.CBI and self.config[0].cbl_apply_transform_on_bulk_operation: + if ( + dest_style == MetaDataStyle.CBI + and self.config[0].Comic_Book_Lover_apply_transform_on_bulk_operation + ): md = CBLTransformer(md, self.config[0]).apply() if not ca.write_metadata(md, dest_style): @@ -1674,7 +1677,7 @@ class TaggerWindow(QtWidgets.QMainWindow): logger.exception("Save aborted.") if not ct_md.is_empty: - if self.config[0].cbl_apply_transform_on_import: + if self.config[0].Comic_Book_Lover_apply_transform_on_import: ct_md = CBLTransformer(ct_md, self.config[0]).apply() QtWidgets.QApplication.restoreOverrideCursor() @@ -1704,10 +1707,10 @@ class TaggerWindow(QtWidgets.QMainWindow): logger.error("Failed to load metadata for %s: %s", ca.path, e) if md.is_empty: md = ca.metadata_from_filename( - self.config[0].filename_complicated_parser, - self.config[0].filename_remove_c2c, - self.config[0].filename_remove_fcbd, - self.config[0].filename_remove_publisher, + self.config[0].Filename_Parsing_complicated_parser, + self.config[0].Filename_Parsing_remove_c2c, + self.config[0].Filename_Parsing_remove_fcbd, + self.config[0].Filename_Parsing_remove_publisher, dlg.split_words, ) if dlg.ignore_leading_digits_in_filename and md.series is not None: @@ -1793,7 +1796,7 @@ class TaggerWindow(QtWidgets.QMainWindow): ) md.overlay(ct_md.replace(notes=utils.combine_notes(md.notes, notes, "Tagged with ComicTagger"))) - if self.config[0].identifier_auto_imprint: + if self.config[0].Issue_Identifier_auto_imprint: md.fix_publisher() if not ca.write_metadata(md, self.save_data_style): @@ -1979,7 +1982,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.config[0].internal_sort_column, self.config[0].internal_sort_direction, ) = self.fileSelectionList.get_sorting() - settngs.save_file(self.config, self.config[0].runtime_config.user_config_dir / "settings.json") + settngs.save_file(self.config, self.config[0].Runtime_Options_config.user_config_dir / "settings.json") event.accept() else: @@ -2106,7 +2109,7 @@ class TaggerWindow(QtWidgets.QMainWindow): self.version_check_complete(version_checker.get_latest_version(self.config[0].internal_install_id)) def version_check_complete(self, new_version: tuple[str, str]) -> None: - if new_version[0] not in (self.version, self.config[0].dialog_dont_notify_about_this_version): + if new_version[0] not in (self.version, self.config[0].Dialog_Flags_dont_notify_about_this_version): website = "https://github.com/comictagger/comictagger" checked = OptionalMessageDialog.msg( self, @@ -2117,7 +2120,7 @@ class TaggerWindow(QtWidgets.QMainWindow): "Don't tell me about this version again", ) if checked: - self.config[0].dialog_dont_notify_about_this_version = new_version[0] + self.config[0].Dialog_Flags_dont_notify_about_this_version = new_version[0] def on_incoming_socket_connection(self) -> None: # Accept connection from other instance. diff --git a/comictaggerlib/ui/talkeruigenerator.py b/comictaggerlib/ui/talkeruigenerator.py index 9d97611..85e3cc4 100644 --- a/comictaggerlib/ui/talkeruigenerator.py +++ b/comictaggerlib/ui/talkeruigenerator.py @@ -2,12 +2,12 @@ from __future__ import annotations import logging from functools import partial -from typing import Any, NamedTuple +from typing import Any, NamedTuple, cast import settngs from PyQt5 import QtCore, QtGui, QtWidgets -from comictaggerlib.ctsettings import ct_ns +from comictaggerlib.ctsettings import ct_ns, group_for_plugin from comictaggerlib.graphics import graphics_path from comictalker.comictalker import ComicTalker @@ -16,9 +16,15 @@ logger = logging.getLogger(__name__) class TalkerTab(NamedTuple): tab: QtWidgets.QWidget + # dict[option.dest] = QWidget widgets: dict[str, QtWidgets.QWidget] +class Sources(NamedTuple): + cbx_sources: QtWidgets.QComboBox + tabs: list[tuple[ComicTalker, TalkerTab]] + + class PasswordEdit(QtWidgets.QLineEdit): """ Password LineEdit with icons to show/hide password entries. @@ -38,11 +44,11 @@ class PasswordEdit(QtWidgets.QLineEdit): # Add the password hide/shown toggle at the end of the edit box. self.togglepasswordAction = self.addAction(self.visibleIcon, QtWidgets.QLineEdit.TrailingPosition) self.togglepasswordAction.setToolTip("Show password") - self.togglepasswordAction.triggered.connect(self.on_toggle_password_Action) + self.togglepasswordAction.triggered.connect(self.on_toggle_password_action) self.password_shown = False - def on_toggle_password_Action(self) -> None: + def on_toggle_password_action(self) -> None: if not self.password_shown: self.setEchoMode(QtWidgets.QLineEdit.Normal) self.password_shown = True @@ -56,14 +62,16 @@ class PasswordEdit(QtWidgets.QLineEdit): def generate_api_widgets( - talker_id: str, - sources: dict[str, QtWidgets.QWidget], - config: settngs.Config[ct_ns], + talker: ComicTalker, + widgets: TalkerTab, + key_option: settngs.Setting, + url_option: settngs.Setting, layout: QtWidgets.QGridLayout, - talkers: dict[str, ComicTalker], ) -> None: # *args enforces keyword arguments and allows position arguments to be ignored - def call_check_api(*args: Any, le_url: QtWidgets.QLineEdit, le_key: QtWidgets.QLineEdit, talker_id: str) -> None: + def call_check_api( + *args: Any, le_url: QtWidgets.QLineEdit, le_key: QtWidgets.QLineEdit, talker: ComicTalker + ) -> None: url = "" key = "" if le_key is not None: @@ -71,46 +79,43 @@ def generate_api_widgets( if le_url is not None: url = le_url.text().strip() - check_text, check_bool = talkers[talker_id].check_api_key(url, key) + check_text, check_bool = talker.check_api_key(url, key) if check_bool: QtWidgets.QMessageBox.information(None, "API Test Success", check_text) else: QtWidgets.QMessageBox.warning(None, "API Test Failed", check_text) # get the actual config objects in case they have overwritten the default - talker_key = config[1][f"talker_{talker_id}"][1][f"{talker_id}_key"] - talker_url = config[1][f"talker_{talker_id}"][1][f"{talker_id}_url"] btn_test_row = None le_key = None le_url = None # only file settings are saved - if talker_key.file: - # record the current row so we know where to add the button + if key_option.file: + # record the current row, so we know where to add the button btn_test_row = layout.rowCount() - le_key = generate_password_textbox(talker_key, layout) + le_key = generate_password_textbox(key_option, layout) # To enable setting and getting - sources["tabs"][talker_id].widgets[f"talker_{talker_id}_{talker_id}_key"] = le_key + widgets.widgets[key_option.dest] = le_key # only file settings are saved - if talker_url.file: - # record the current row so we know where to add the button + if url_option.file: + # record the current row, so we know where to add the button # We overwrite so that the default will be next to the url text box btn_test_row = layout.rowCount() - le_url = generate_textbox(talker_url, layout) - value, _ = settngs.get_option(config[0], talker_url) - if not value: - le_url.setText(talkers[talker_id].default_api_url) + le_url = generate_textbox(url_option, layout) + # We insert the default url here so that people don't think it's unset + le_url.setText(talker.default_api_url) # To enable setting and getting - sources["tabs"][talker_id].widgets[f"talker_{talker_id}_{talker_id}_url"] = le_url + widgets.widgets[url_option.dest] = le_url # The button row was recorded so we add it if btn_test_row is not None: btn = QtWidgets.QPushButton("Test API") layout.addWidget(btn, btn_test_row, 2) # partial is used as connect will pass in event information - btn.clicked.connect(partial(call_check_api, le_url=le_url, le_key=le_key, talker_id=talker_id)) + btn.clicked.connect(partial(call_check_api, le_url=le_url, le_key=le_key, talker=talker)) def generate_checkbox(option: settngs.Setting, layout: QtWidgets.QGridLayout) -> QtWidgets.QCheckBox: @@ -171,31 +176,39 @@ def generate_password_textbox(option: settngs.Setting, layout: QtWidgets.QGridLa return widget -def settings_to_talker_form(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[ct_ns]) -> None: +def settings_to_talker_form(sources: Sources, config: settngs.Config[ct_ns]) -> None: # Set the active talker via id in sources combo box - sources["cbx_select_talker"].setCurrentIndex(sources["cbx_select_talker"].findData(config[0].talker_source)) + sources[0].setCurrentIndex(sources[0].findData(config[0].Sources_source)) - for talker in sources["tabs"].items(): - for name, widget in talker[1].widgets.items(): - value = getattr(config[0], name) - value_type = type(value) + # Iterate over the tabs, the talker is included in the tab so no extra lookup is needed + for talker, tab in sources.tabs: + # dest is guaranteed to be unique within a talker + # and refer to the correct item in config.definitions.v['group name'] + for dest, widget in tab.widgets.items(): + value, default = settngs.get_option(config.values, config.definitions[group_for_plugin(talker)].v[dest]) try: - if value_type is str and value: + if isinstance(value, str) and value and isinstance(widget, QtWidgets.QLineEdit) and not default: widget.setText(value) - if value_type is int or value_type is float: + if isinstance(value, (float, int)) and isinstance( + widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox) + ): widget.setValue(value) - if value_type is bool: + if isinstance(value, bool) and isinstance(widget, QtWidgets.QCheckBox): widget.setChecked(value) except Exception: - logger.debug("Failed to set value of %s", name) + logger.debug("Failed to set value of %s for %s(%s)", dest, talker.name, talker.id) -def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settngs.Config[ct_ns]) -> None: - # Source combo box value - config[0].talker_source = sources["cbx_select_talker"].currentData() +def form_settings_to_config(sources: Sources, config: settngs.Config) -> settngs.Config[ct_ns]: + # Update the currently selected talker + config.values.Sources_source = sources.cbx_sources.currentData() + cfg = settngs.normalize_config(config, True, True) - for tab in sources["tabs"].items(): - for name, widget in tab[1].widgets.items(): + # Iterate over the tabs, the talker is included in the tab so no extra lookup is needed + for talker, tab in sources.tabs: + talker_options = cfg.values[group_for_plugin(talker)] + # dest is guaranteed to be unique within a talker and refer to the correct item in config.values['group name'] + for dest, widget in tab.widgets.items(): widget_value = None if isinstance(widget, (QtWidgets.QSpinBox, QtWidgets.QDoubleSpinBox)): widget_value = widget.value() @@ -204,83 +217,88 @@ def form_settings_to_config(sources: dict[str, QtWidgets.QWidget], config: settn elif isinstance(widget, QtWidgets.QCheckBox): widget_value = widget.isChecked() - setattr(config[0], name, widget_value) + talker_options[dest] = widget_value + return cast(settngs.Config[ct_ns], settngs.get_namespace(cfg, True, True)) def generate_source_option_tabs( comic_talker_tab: QtWidgets.QWidget, config: settngs.Config[ct_ns], talkers: dict[str, ComicTalker], -) -> dict[str, QtWidgets.QWidget]: +) -> Sources: """ Generate GUI tabs and settings for talkers """ - # Store all widgets as to allow easier access to their values vs. using findChildren etc. on the tab widget - sources: dict = {"tabs": {}} - # Tab comes with a QVBoxLayout comic_talker_tab_layout = comic_talker_tab.layout() talker_layout = QtWidgets.QGridLayout() lbl_select_talker = QtWidgets.QLabel("Metadata Source:") - cbx_select_talker = QtWidgets.QComboBox() line = QtWidgets.QFrame() line.setFrameShape(QtWidgets.QFrame.HLine) line.setFrameShadow(QtWidgets.QFrame.Sunken) talker_tabs = QtWidgets.QTabWidget() + # Store all widgets as to allow easier access to their values vs. using findChildren etc. on the tab widget + sources: Sources = Sources(QtWidgets.QComboBox(), []) + talker_layout.addWidget(lbl_select_talker, 0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) - talker_layout.addWidget(cbx_select_talker, 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) + talker_layout.addWidget(sources[0], 0, 1, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Maximum) talker_layout.addWidget(line, 1, 0, 1, -1) talker_layout.addWidget(talker_tabs, 2, 0, 1, -1) comic_talker_tab_layout.addLayout(talker_layout) - # Add combobox to sources for getting and setting talker - sources["cbx_select_talker"] = cbx_select_talker - # Add source sub tabs to Comic Sources tab - for talker_id, talker_obj in talkers.items(): + for t_id, talker in talkers.items(): # Add source to general tab dropdown list - cbx_select_talker.addItem(talker_obj.name, talker_id) + sources.cbx_sources.addItem(talker.name, t_id) + tab = TalkerTab(tab=QtWidgets.QWidget(), widgets={}) - tab_name = talker_id - sources["tabs"][tab_name] = TalkerTab(tab=QtWidgets.QWidget(), widgets={}) layout_grid = QtWidgets.QGridLayout() - - for option in config[1][f"talker_{talker_id}"][1].values(): + url_option: settngs.Setting | None = None + key_option: settngs.Setting | None = None + for option in config.definitions[group_for_plugin(talker)].v.values(): if not option.file: continue - if option.dest in (f"{talker_id}_url", f"{talker_id}_key"): - continue - current_widget = None - if option._guess_type() is bool: + elif option.dest == f"{t_id}_key": + key_option = option + elif option.dest == f"{t_id}_url": + url_option = option + elif option._guess_type() is bool: current_widget = generate_checkbox(option, layout_grid) - sources["tabs"][tab_name].widgets[option.internal_name] = current_widget + tab.widgets[option.dest] = current_widget elif option._guess_type() is int: current_widget = generate_spinbox(option, layout_grid) - sources["tabs"][tab_name].widgets[option.internal_name] = current_widget + tab.widgets[option.dest] = current_widget elif option._guess_type() is float: current_widget = generate_doublespinbox(option, layout_grid) - sources["tabs"][tab_name].widgets[option.internal_name] = current_widget + tab.widgets[option.dest] = current_widget elif option._guess_type() is str: current_widget = generate_textbox(option, layout_grid) - sources["tabs"][tab_name].widgets[option.internal_name] = current_widget + tab.widgets[option.dest] = current_widget else: logger.debug(f"Unsupported talker option found. Name: {option.internal_name} Type: {option.type}") + # The key and url options are always defined. + # If they aren't something has gone wrong with the talker, remove it + if key_option is None or url_option is None: + del talkers[t_id] + continue + # Add talker URL and API key fields - generate_api_widgets(talker_id, sources, config, layout_grid, talkers) + generate_api_widgets(talker, tab, key_option, url_option, layout_grid) # Add vertical spacer vspacer = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) layout_grid.addItem(vspacer, layout_grid.rowCount() + 1, 0) # Display the new widgets - sources["tabs"][tab_name].tab.setLayout(layout_grid) + tab.tab.setLayout(layout_grid) # Add new sub tab to Comic Source tab - talker_tabs.addTab(sources["tabs"][tab_name].tab, talker_obj.name) + talker_tabs.addTab(tab.tab, talker.name) + sources.tabs.append((talker, tab)) return sources diff --git a/setup.cfg b/setup.cfg index 89b4174..418ebf4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,7 @@ install_requires = pyrate-limiter>=2.6,<3 rapidfuzz>=2.12.0 requests==2.* - settngs==0.7.1 + settngs==0.7.2 text2digits typing-extensions>=4.3.0 wordninja diff --git a/tests/comiccacher_test.py b/tests/comiccacher_test.py index d9c3114..6baf56e 100644 --- a/tests/comiccacher_test.py +++ b/tests/comiccacher_test.py @@ -10,8 +10,8 @@ from testing.comicdata import search_results def test_create_cache(config, mock_version): config, definitions = config - comictalker.comiccacher.ComicCacher(config.runtime_config.user_cache_dir, mock_version[0]) - assert config.runtime_config.user_cache_dir.exists() + comictalker.comiccacher.ComicCacher(config.Runtime_Options_config.user_cache_dir, mock_version[0]) + assert config.Runtime_Options_config.user_cache_dir.exists() def test_search_results(comic_cache): diff --git a/tests/conftest.py b/tests/conftest.py index b01e141..e150d15 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -117,7 +117,7 @@ def comicvine_api(monkeypatch, cbz, comic_cache, mock_version, config) -> comict cv = comictalker.talkers.comicvine.ComicVineTalker( version=mock_version[0], - cache_folder=config[0].runtime_config.user_cache_dir, + cache_folder=config[0].Runtime_Options_config.user_cache_dir, ) manager = settngs.Manager() manager.add_persistent_group("comicvine", cv.register_settings) @@ -174,14 +174,14 @@ def config(tmp_path): app.register_settings() defaults = app.parse_settings(comictaggerlib.ctsettings.ComicTaggerPaths(tmp_path / "config"), "") - defaults[0].runtime_config.user_data_dir.mkdir(parents=True, exist_ok=True) - defaults[0].runtime_config.user_config_dir.mkdir(parents=True, exist_ok=True) - defaults[0].runtime_config.user_cache_dir.mkdir(parents=True, exist_ok=True) - defaults[0].runtime_config.user_state_dir.mkdir(parents=True, exist_ok=True) - defaults[0].runtime_config.user_log_dir.mkdir(parents=True, exist_ok=True) + defaults[0].Runtime_Options_config.user_data_dir.mkdir(parents=True, exist_ok=True) + defaults[0].Runtime_Options_config.user_config_dir.mkdir(parents=True, exist_ok=True) + defaults[0].Runtime_Options_config.user_cache_dir.mkdir(parents=True, exist_ok=True) + defaults[0].Runtime_Options_config.user_state_dir.mkdir(parents=True, exist_ok=True) + defaults[0].Runtime_Options_config.user_log_dir.mkdir(parents=True, exist_ok=True) yield defaults @pytest.fixture def comic_cache(config, mock_version) -> Generator[comictalker.comiccacher.ComicCacher, Any, None]: - yield comictalker.comiccacher.ComicCacher(config[0].runtime_config.user_cache_dir, mock_version[0]) + yield comictalker.comiccacher.ComicCacher(config[0].Runtime_Options_config.user_cache_dir, mock_version[0])