diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a40104d..0a068b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: args: ["--diff"] - repo: https://github.com/pycqa/flake8 - rev: 4.0.1 + rev: 6.1.0 hooks: - id: flake8 @@ -29,7 +29,7 @@ repos: - id: pylint language: python types: [python] - additional_dependencies: [aqt] + additional_dependencies: [aqt, pytest] args: [ "-rn", # Only display messages diff --git a/ankiweb_description.html b/ankiweb_description.html index 22556df..4351f30 100644 --- a/ankiweb_description.html +++ b/ankiweb_description.html @@ -106,7 +106,7 @@ 2024-01-19: Fix bug in Sketchy-Cloze note type front template -2024-04-08: Add the AnKingAnkisthesia note type - Add a reset button for general settings - Fix cloze color when using b/u/i styling - Fix extra color when using italics styling - Add night mode tables to AnKingMCAT +2024-04-08: Add the AnKingAnkisthesia note type - Add a reset button for general settings - Fix cloze color when using b/u/i styling - Fix extra color when using italics styling - Add night mode tables to AnKing MCAT 2024-04-18: Revert img max height/width for IO one by one to 100% - Change min-width of images in Extra field to 0% diff --git a/src/anking_notetypes/gui/config_window.py b/src/anking_notetypes/gui/config_window.py index 3327659..7e4c6c7 100644 --- a/src/anking_notetypes/gui/config_window.py +++ b/src/anking_notetypes/gui/config_window.py @@ -10,7 +10,11 @@ from ..ankiaddonconfig import ConfigManager, ConfigWindow from ..ankiaddonconfig.window import ConfigLayout -from ..constants import ANKIHUB_NOTETYPE_RE, NOTETYPE_COPY_RE +from ..notetype_renames import ( + canonical_notetype_name, + legacy_notetype_names, + matching_notetype_names, +) from ..notetype_setting import NotetypeSetting, NotetypeSettingException from ..notetype_setting_definitions import ( anking_notetype_model, @@ -18,8 +22,10 @@ configurable_fields_for_notetype, general_settings, general_settings_defaults_dict, - setting_configs, + is_ankihub_notetype_version, + is_notetype_copy, notetype_base_name, + setting_configs, ) from ..utils import update_notetype_to_newest_version from .anking_widgets import AnkingIconsLayout, GithubLinkLayout @@ -390,7 +396,7 @@ def _reset_notetype_and_reload_ui(self, model: "NotetypeDict"): return nt_base_name = notetype_base_name(model["name"]) - for model_version in _note_type_versions(model["name"]): + for model_version in _note_type_versions(nt_base_name): update_notetype_to_newest_version(model_version, nt_base_name) mw.col.models.update_dict(model_version) # type: ignore @@ -631,26 +637,45 @@ def _note_type_versions(nt_base_name: str) -> List["NotetypeDict"]: """Returns a list of all notetype versions of the notetype in the collection. Version of a note type are created by the AnkiHub add-on and by copying the base AnKing note types or importing them from different sources.""" + matching_names = matching_notetype_names(canonical_notetype_name(nt_base_name)) models = [ mw.col.models.get(x.id) # type: ignore for x in mw.col.models.all_names_and_ids() - if x.name == nt_base_name - or re.match(ANKIHUB_NOTETYPE_RE.format(notetype_base_name=nt_base_name), x.name) - or re.match(NOTETYPE_COPY_RE.format(notetype_base_name=nt_base_name), x.name) + for matching_name in matching_names + if _matches_notetype_version(x.name, matching_name) ] return models +def _matches_notetype_version(model_name: str, base_name: str) -> bool: + return ( + model_name == base_name + or is_ankihub_notetype_version(model_name, base_name) + or is_notetype_copy(model_name, base_name) + ) + + def _most_basic_notetype_version(nt_base_name: str) -> Optional["NotetypeDict"]: - """Returns the most basic version of a note type, that is the version with the shortest name.""" + """Returns the most basic version of a note type. + + Prefers an exact match on the canonical name, then an exact legacy name, + then falls back to the shortest name. Without the canonical/legacy + preference, a legacy main like ``AnKingMCAT`` would beat ``AnKing MCAT`` + on name length alone. + """ + canonical = canonical_notetype_name(nt_base_name) model_versions = _note_type_versions(nt_base_name) - result = min( + versions_by_name = {model["name"]: model for model in model_versions} + + for preferred in [canonical, *legacy_notetype_names(canonical)]: + if preferred in versions_by_name: + return versions_by_name[preferred] + + return min( model_versions, - # sort by length of name and then alphabetically key=lambda model: (len(model["name"]), model["name"]), default=None, ) - return result def _names_of_all_supported_note_types() -> List[str]: diff --git a/src/anking_notetypes/gui/extra_notetype_versions.py b/src/anking_notetypes/gui/extra_notetype_versions.py index 6ef6176..88e0d2a 100644 --- a/src/anking_notetypes/gui/extra_notetype_versions.py +++ b/src/anking_notetypes/gui/extra_notetype_versions.py @@ -1,42 +1,50 @@ -import re from concurrent.futures import Future from copy import deepcopy -from typing import Dict, List +from typing import TYPE_CHECKING, Dict, List, Optional, Tuple from aqt import mw from aqt.utils import askUser, tooltip -from ..constants import NOTETYPE_COPY_RE -from ..notetype_setting_definitions import anking_notetype_names +from ..notetype_renames import legacy_notetype_names, matching_notetype_names +from ..notetype_setting_definitions import anking_notetype_names, is_notetype_copy from ..utils import adjust_fields, create_backup +if TYPE_CHECKING: + from anki.models import NotetypeDict + def handle_extra_notetype_versions() -> None: - # mids of copies of the AnKing notetype identified by its name + # mids of copies of the AnKing notetype, keyed by canonical base name copy_mids_by_notetype_base_name: Dict[str, List[int]] = dict() + # (legacy_name, canonical_name) pairs for mains that will be renamed during conversion + legacy_mains_to_rename: List[Tuple[str, str]] = [] + all_models = list(mw.col.models.all_names_and_ids()) for notetype_base_name in anking_notetype_names(): - if mw.col.models.by_name(notetype_base_name) is None: + matching_names = matching_notetype_names(notetype_base_name) + if _first_existing_notetype_name(matching_names) is None: continue notetype_copy_mids = [ x.id - for x in mw.col.models.all_names_and_ids() - if re.match( - NOTETYPE_COPY_RE.format(notetype_base_name=notetype_base_name), x.name - ) + for x in all_models + for matching_name in matching_names + if is_notetype_copy(x.name, matching_name) ] - if notetype_copy_mids: - copy_mids_by_notetype_base_name[notetype_base_name] = notetype_copy_mids + if not notetype_copy_mids: + continue + + copy_mids_by_notetype_base_name[notetype_base_name] = notetype_copy_mids + if mw.col.models.by_name(notetype_base_name) is None: + for legacy_name in legacy_notetype_names(notetype_base_name): + if mw.col.models.by_name(legacy_name) is not None: + legacy_mains_to_rename.append((legacy_name, notetype_base_name)) + break if not copy_mids_by_notetype_base_name: return if not askUser( - "There are extra copies of AnKing note types. Do you want to convert all note types with names like " - 'for example "AnKingOverhaul-1dgs0" to "AnKingOverhaul" respectively?\n\n' - "This will delete the extra note types and require a full upload of the collection " - "the next time you sync with AnkiWeb. A backup will be created before the changes are applied.\n\n" - "No matter what you chose the AnKing Note Types window will open after you select an option.", + _build_confirmation_message(legacy_mains_to_rename), title="Extra copies of AnKing note types", ): return @@ -63,6 +71,8 @@ def convert_extra_notetypes( for notetype_base_name, copy_mids in copy_mids_by_notetype_base_name.items(): model = mw.col.models.by_name(notetype_base_name) + if model is None: + model = _rename_legacy_main_to_canonical(notetype_base_name) for copy_mid in copy_mids: model_copy = mw.col.models.get(copy_mid) # type: ignore @@ -92,3 +102,52 @@ def convert_extra_notetypes( mw.reset() tooltip("Note types were converted successfully.") + + +def _build_confirmation_message( + legacy_mains_to_rename: List[Tuple[str, str]], +) -> str: + message = ( + "There are extra copies of AnKing note types. Do you want to convert all " + "note types with names like " + 'for example "AnKingOverhaul-1dgs0" to "AnKingOverhaul" respectively?\n\n' + "This will delete the extra note types and require a full upload of the " + "collection the next time you sync with AnkiWeb. A backup will be created " + "before the changes are applied.\n\n" + ) + if legacy_mains_to_rename: + renames = "\n".join( + f' - "{legacy}" → "{canonical}"' + for legacy, canonical in legacy_mains_to_rename + ) + message += ( + "The following note types will also be renamed to their current " + f"names:\n{renames}\n\n" + ) + message += ( + "No matter what you chose the AnKing Note Types window will open after " + "you select an option." + ) + return message + + +def _first_existing_notetype_name(notetype_names: List[str]) -> Optional[str]: + return next( + ( + notetype_name + for notetype_name in notetype_names + if mw.col.models.by_name(notetype_name) is not None + ), + None, + ) + + +def _rename_legacy_main_to_canonical(canonical_name: str) -> Optional["NotetypeDict"]: + for legacy_name in legacy_notetype_names(canonical_name): + legacy_model = mw.col.models.by_name(legacy_name) + if legacy_model is None: + continue + legacy_model["name"] = canonical_name + mw.col.models.update_dict(legacy_model) + return legacy_model + return None diff --git a/src/anking_notetypes/note_types/AnKingMCAT/AnKingMCAT.json b/src/anking_notetypes/note_types/AnKingMCAT/AnKingMCAT.json index 9abfd34..a85323e 100644 --- a/src/anking_notetypes/note_types/AnKingMCAT/AnKingMCAT.json +++ b/src/anking_notetypes/note_types/AnKingMCAT/AnKingMCAT.json @@ -1,6 +1,6 @@ { "id": 1610414929688, - "name": "AnKingMCAT", + "name": "AnKing MCAT", "type": 1, "mod": 1638144691, "usn": -1, @@ -117,4 +117,4 @@ ], "tags": [], "vers": [] -} \ No newline at end of file +} diff --git a/src/anking_notetypes/note_types/AnKingMCAT/README.md b/src/anking_notetypes/note_types/AnKingMCAT/README.md index 91091a1..1acef09 100644 --- a/src/anking_notetypes/note_types/AnKingMCAT/README.md +++ b/src/anking_notetypes/note_types/AnKingMCAT/README.md @@ -1,5 +1,5 @@ ### Features Unique to this Note Type -- AnKingMed alternate styling available here +- AnKingMed alternate styling available here
Replace the customizable portion with the contents of the link above (in styling)

diff --git a/src/anking_notetypes/notetype_renames.py b/src/anking_notetypes/notetype_renames.py new file mode 100644 index 0000000..66ba179 --- /dev/null +++ b/src/anking_notetypes/notetype_renames.py @@ -0,0 +1,37 @@ +import re +from typing import Dict, List + + +# Add note type renames here as "old bundled name": "new bundled name". +# The old name is still used to find existing note types in users' collections. +NOTETYPE_RENAMES: Dict[str, str] = { + "AnKingMCAT": "AnKing MCAT", +} + + +def canonical_notetype_name(notetype_name: str) -> str: + return NOTETYPE_RENAMES.get(notetype_name, notetype_name) + + +def legacy_notetype_names(canonical_name: str) -> List[str]: + return [ + old_name + for old_name, new_name in NOTETYPE_RENAMES.items() + if new_name == canonical_name + ] + + +def matching_notetype_names(canonical_name: str) -> List[str]: + return [canonical_name, *legacy_notetype_names(canonical_name)] + + +def renamed_notetype_name(model_name: str) -> str: + # Match bare name or copy-suffixed form only. AnkiHub-qualified forms + # like "AnKingMCAT (AnKing-MCAT / AnKingMed)" are owned by the AnkiHub + # add-on — we don't rename them here because the deck portion may or + # may not be renamed upstream, and we can't know. + for old_name, new_name in NOTETYPE_RENAMES.items(): + match = re.match(rf"({re.escape(old_name)})(?=$|-)", model_name) + if match: + return new_name + model_name[match.end() :] + return model_name diff --git a/src/anking_notetypes/notetype_setting_definitions.py b/src/anking_notetypes/notetype_setting_definitions.py index 7088428..fbacd3a 100644 --- a/src/anking_notetypes/notetype_setting_definitions.py +++ b/src/anking_notetypes/notetype_setting_definitions.py @@ -3,6 +3,13 @@ from pathlib import Path from typing import Any, Dict, List, OrderedDict, Tuple, Union +from .constants import ANKIHUB_NOTETYPE_RE, NOTETYPE_COPY_RE +from .notetype_renames import ( + canonical_notetype_name, + legacy_notetype_names, + matching_notetype_names, +) + try: from anki.models import NotetypeDict # pylint: disable=unused-import except: @@ -675,7 +682,7 @@ def anking_notetype_templates() -> Dict[str, Tuple[str, str, str]]: for x in ANKING_NOTETYPES_PATH.iterdir(): if not x.is_dir(): continue - notetype_name = x.name + notetype_name = canonical_notetype_name(x.name) front_template = (x / "Front Template.html").read_text( encoding="utf-8", errors="ignore" @@ -690,16 +697,34 @@ def anking_notetype_templates() -> Dict[str, Tuple[str, str, str]]: def anking_notetype_model(notetype_name: str) -> "NotetypeDict": + notetype_name = canonical_notetype_name(notetype_name) + notetype_folder_name = _notetype_folder_name(notetype_name) result = json.loads( - (ANKING_NOTETYPES_PATH / notetype_name / f"{notetype_name}.json").read_text() + ( + ANKING_NOTETYPES_PATH + / notetype_folder_name + / f"{notetype_folder_name}.json" + ).read_text() ) front, back, styling = anking_notetype_templates()[notetype_name] + result["name"] = notetype_name result["tmpls"][0]["qfmt"] = front result["tmpls"][0]["afmt"] = back result["css"] = styling return result +def _notetype_folder_name(notetype_name: str) -> str: + if (ANKING_NOTETYPES_PATH / notetype_name).is_dir(): + return notetype_name + + for legacy_name in legacy_notetype_names(notetype_name): + if (ANKING_NOTETYPES_PATH / legacy_name).is_dir(): + return legacy_name + + return notetype_name + + def anking_notetype_models() -> List["NotetypeDict"]: return [anking_notetype_model(name) for name in anking_notetype_names()] @@ -707,16 +732,42 @@ def anking_notetype_models() -> List["NotetypeDict"]: def notetype_base_name(model_name: str) -> str: """Returns the base name of a note type, that is if it's a version of a an anking note type it will return the base name, otherwise it will return the name itself.""" + candidates = [ + (matching_name, base_name) + for base_name in anking_notetype_names() + for matching_name in matching_notetype_names(base_name) + ] + # Prefer the longest matching name so e.g. "AnKing MCAT" wins over "AnKing" + # when the model is "AnKing MCAT" / "AnKing MCAT-abcde" / AnkiHub-qualified. + candidates.sort(key=lambda pair: len(pair[0]), reverse=True) return next( ( - notetype_base_name - for notetype_base_name in anking_notetype_names() - if re.match(rf"{notetype_base_name}($| |-)", model_name) + base_name + for matching_name, base_name in candidates + if re.match(rf"{re.escape(matching_name)}($| |-)", model_name) ), None, ) +def is_notetype_copy(model_name: str, base_name: str) -> bool: + return bool( + re.match( + NOTETYPE_COPY_RE.format(notetype_base_name=re.escape(base_name)), + model_name, + ) + ) + + +def is_ankihub_notetype_version(model_name: str, base_name: str) -> bool: + return bool( + re.match( + ANKIHUB_NOTETYPE_RE.format(notetype_base_name=re.escape(base_name)), + model_name, + ) + ) + + def is_io_note_type(model_name: str) -> bool: "Return True if the given note type is an image occlusion type." return notetype_base_name(model_name) in ("IO-one by one", "Physeo-IO one by one") diff --git a/src/anking_notetypes/utils.py b/src/anking_notetypes/utils.py index 8e7f658..c4d4a52 100644 --- a/src/anking_notetypes/utils.py +++ b/src/anking_notetypes/utils.py @@ -12,6 +12,7 @@ ANKIHUB_HTML_END_COMMENT_RE, ANKIHUB_TEMPLATE_SNIPPET_RE, ) +from .notetype_renames import renamed_notetype_name from .notetype_setting_definitions import anking_notetype_model try: @@ -25,7 +26,7 @@ def update_notetype_to_newest_version( ) -> None: new_model = anking_notetype_model(notetype_base_name) new_model["id"] = model["id"] - new_model["name"] = model["name"] # keep the name + new_model["name"] = _updated_notetype_name(model["name"]) new_model["mod"] = int(time.time()) # not sure if this is needed new_model["usn"] = -1 # triggers full sync @@ -41,6 +42,13 @@ def update_notetype_to_newest_version( model.update(new_model) +def _updated_notetype_name(model_name: str) -> str: + new_name = renamed_notetype_name(model_name) + if new_name == model_name or mw.col.models.by_name(new_name): + return model_name + return new_name + + def _retain_ankihub_modifications( old_model: "NotetypeDict", new_model: "NotetypeDict" ) -> "NotetypeDict": diff --git a/tests/expected.json b/tests/expected.json index d73237a..b1efd39 100644 --- a/tests/expected.json +++ b/tests/expected.json @@ -323,7 +323,7 @@ "user_action_6": "undefined", "user_action_7": "undefined", "user_action_8": "undefined"}, - "AnKingMCAT": { + "AnKing MCAT": { "always_one_by_one": false, "autoflip": false, "autoreveal_additional_resources": false, diff --git a/tests/test_unit.py b/tests/test_unit.py index 3101f03..0f13e67 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -1,4 +1,20 @@ +# pylint: disable=protected-access +from unittest.mock import MagicMock, patch + +import pytest + +from src.anking_notetypes import notetype_setting_definitions, utils +from src.anking_notetypes.gui import extra_notetype_versions +from src.anking_notetypes.notetype_renames import ( + NOTETYPE_RENAMES, + canonical_notetype_name, + matching_notetype_names, + renamed_notetype_name, +) from src.anking_notetypes.notetype_setting import order_names +from src.anking_notetypes.notetype_setting_definitions import notetype_base_name + +FAKE_RENAMES = {"Old-AnKing": "AnKingOverhaul"} class TestOrderNames: @@ -25,3 +41,203 @@ def test_no_matches(self): assert order_names( new_names=["Apple", "Banana"], current_names=["Cherry", "Date"] ) == ["Apple", "Banana"] + + +class TestNotetypeRenames: + def test_mcat_legacy_name_maps_to_new_name(self): + assert canonical_notetype_name("AnKingMCAT") == "AnKing MCAT" + assert notetype_base_name("AnKingMCAT") == "AnKing MCAT" + assert ( + notetype_base_name("AnKingMCAT (AnKing-MCAT / AnKingMed)") == "AnKing MCAT" + ) + + def test_mcat_canonical_name_not_confused_with_anking_prefix(self): + # "AnKing" is a valid base and prefix of "AnKing MCAT" — verify the + # longer name wins so canonical MCAT names are not misclassified. + assert notetype_base_name("AnKing MCAT") == "AnKing MCAT" + assert notetype_base_name("AnKing MCAT-abcde") == "AnKing MCAT" + assert ( + notetype_base_name("AnKing MCAT (AnKing MCAT Deck / AnKingMed)") + == "AnKing MCAT" + ) + + def test_renamed_notetype_name_leaves_ankihub_qualified_alone(self): + # AnkiHub-qualified notetypes are renamed by the AnkiHub add-on, not here. + assert ( + renamed_notetype_name("AnKingMCAT (AnKing-MCAT / AnKingMed)") + == "AnKingMCAT (AnKing-MCAT / AnKingMed)" + ) + + def test_renamed_notetype_name_ignores_unrelated_prefix(self): + with patch.dict(NOTETYPE_RENAMES, FAKE_RENAMES): + assert renamed_notetype_name("Old-AnKingology") == "Old-AnKingology" + + def test_canonical_notetype_name(self): + with patch.dict(NOTETYPE_RENAMES, FAKE_RENAMES): + assert canonical_notetype_name("Old-AnKing") == "AnKingOverhaul" + assert canonical_notetype_name("AnKingOverhaul") == "AnKingOverhaul" + + def test_matching_notetype_names(self): + with patch.dict(NOTETYPE_RENAMES, FAKE_RENAMES): + assert matching_notetype_names("AnKingOverhaul") == [ + "AnKingOverhaul", + "Old-AnKing", + ] + + def test_renamed_notetype_name(self): + with patch.dict(NOTETYPE_RENAMES, FAKE_RENAMES): + assert renamed_notetype_name("Old-AnKing") == "AnKingOverhaul" + assert renamed_notetype_name("Old-AnKing-1dgs0") == "AnKingOverhaul-1dgs0" + # AnkiHub-qualified form is left alone — AnkiHub owns that rename. + assert ( + renamed_notetype_name("Old-AnKing (AnKing / Example)") + == "Old-AnKing (AnKing / Example)" + ) + + def test_notetype_base_name_recognizes_legacy_name(self): + with patch.dict(NOTETYPE_RENAMES, FAKE_RENAMES): + assert notetype_base_name("Old-AnKing") == "AnKingOverhaul" + assert notetype_base_name("Old-AnKing-1dgs0") == "AnKingOverhaul" + + +@pytest.fixture +def notetypes_path(tmp_path): + with patch.object( + notetype_setting_definitions, "ANKING_NOTETYPES_PATH", tmp_path + ), patch.dict(NOTETYPE_RENAMES, FAKE_RENAMES): + yield tmp_path + + +class TestNotetypeFolderName: + def test_prefers_canonical_folder_when_present(self, notetypes_path): + (notetypes_path / "AnKingOverhaul").mkdir() + (notetypes_path / "Old-AnKing").mkdir() + + assert ( + notetype_setting_definitions._notetype_folder_name("AnKingOverhaul") + == "AnKingOverhaul" + ) + + def test_falls_back_to_legacy_folder(self, notetypes_path): + (notetypes_path / "Old-AnKing").mkdir() + + assert ( + notetype_setting_definitions._notetype_folder_name("AnKingOverhaul") + == "Old-AnKing" + ) + + def test_returns_canonical_when_no_folder_exists( + self, notetypes_path # pylint: disable=unused-argument + ): + assert ( + notetype_setting_definitions._notetype_folder_name("AnKingOverhaul") + == "AnKingOverhaul" + ) + + +class TestUpdatedNotetypeName: + def test_returns_unchanged_when_no_rename_applies(self): + with patch.dict(NOTETYPE_RENAMES, {}, clear=True): + assert utils._updated_notetype_name("Unrelated") == "Unrelated" + + def test_renames_when_new_name_not_in_collection(self): + mw_mock = MagicMock() + mw_mock.col.models.by_name.return_value = None + + with patch.object(utils, "mw", mw_mock), patch.dict( + NOTETYPE_RENAMES, FAKE_RENAMES + ): + assert utils._updated_notetype_name("Old-AnKing") == "AnKingOverhaul" + + def test_keeps_old_name_when_new_name_already_exists(self): + mw_mock = MagicMock() + mw_mock.col.models.by_name.return_value = {"id": 1, "name": "AnKingOverhaul"} + + with patch.object(utils, "mw", mw_mock), patch.dict( + NOTETYPE_RENAMES, FAKE_RENAMES + ): + assert utils._updated_notetype_name("Old-AnKing") == "Old-AnKing" + + +class TestBuildConfirmationMessage: + def test_no_legacy_mains_omits_rename_section(self): + message = extra_notetype_versions._build_confirmation_message([]) + assert "renamed" not in message + + def test_lists_affected_legacy_mains(self): + message = extra_notetype_versions._build_confirmation_message( + [("AnKingMCAT", "AnKing MCAT"), ("Old-AnKing", "AnKingOverhaul")] + ) + assert '"AnKingMCAT" → "AnKing MCAT"' in message + assert '"Old-AnKing" → "AnKingOverhaul"' in message + + +class TestRenameLegacyMainToCanonical: + def test_renames_legacy_main_and_returns_canonical_model(self): + legacy_model = {"id": 42, "name": "Old-AnKing"} + mw_mock = MagicMock() + mw_mock.col.models.by_name.side_effect = lambda name: { + "Old-AnKing": legacy_model, + }.get(name) + + with patch.object(extra_notetype_versions, "mw", mw_mock), patch.dict( + NOTETYPE_RENAMES, FAKE_RENAMES + ): + result = extra_notetype_versions._rename_legacy_main_to_canonical( + "AnKingOverhaul" + ) + + assert legacy_model["name"] == "AnKingOverhaul" + mw_mock.col.models.update_dict.assert_called_once_with(legacy_model) + assert result is legacy_model + + def test_returns_none_when_no_legacy_main_exists(self): + mw_mock = MagicMock() + mw_mock.col.models.by_name.return_value = None + + with patch.object(extra_notetype_versions, "mw", mw_mock), patch.dict( + NOTETYPE_RENAMES, FAKE_RENAMES + ): + assert ( + extra_notetype_versions._rename_legacy_main_to_canonical( + "AnKingOverhaul" + ) + is None + ) + mw_mock.col.models.update_dict.assert_not_called() + + +class TestConvertExtraNotetypes: + def test_canonical_main_present_skips_legacy_rename(self): + canonical_model = { + "id": 1, + "name": "AnKingOverhaul", + "flds": [{"name": "Front"}], + } + copy_model = { + "id": 2, + "name": "AnKingOverhaul-abcde", + "flds": [{"name": "Front"}], + } + mw_mock = MagicMock() + mw_mock.col.models.by_name.side_effect = lambda name: ( + canonical_model if name == "AnKingOverhaul" else None + ) + mw_mock.col.models.get.return_value = copy_model + mw_mock.col.find_notes.return_value = [] + + with patch.object(extra_notetype_versions, "mw", mw_mock), patch.dict( + NOTETYPE_RENAMES, FAKE_RENAMES + ), patch.object( + extra_notetype_versions, "_rename_legacy_main_to_canonical" + ) as rename_mock, patch.object( + extra_notetype_versions, "adjust_fields", side_effect=lambda old, new: new + ), patch.object( + extra_notetype_versions, "tooltip" + ): + extra_notetype_versions.convert_extra_notetypes( + MagicMock(), {"AnKingOverhaul": [2]} + ) + + rename_mock.assert_not_called() + mw_mock.col.models.remove.assert_called_once_with(2)