Repository: Senth/minecraft-mod-manager (python)

Issue:
Show a better message when failing to install a mod because it's filtered
I am trying to install [Inventory HUD+](https://www.curseforge.com/minecraft/mc-mods/inventory-hud-forge) by doing `minecraft-mod-manager install inventory-hud-forge` and it silently exits with this output:

```
inventory-hud-forge
```

It doesn't seem to be installed after that.

Pull request description:
Improve message when failing to install mod
When installing a mod, it would fail silently if all versions were filtered out.

### What changed?
- Added helpful messages when installing
- Refactored so that the logic to filter out is actually inside the business logic and not repo_impl
- The site is no longer set when a version is found. I will add it again later in #27 

### Testing
- [X] Added unit tests
- [X] Test ran the application to see how the printing looked like

### Related Issues
Fixes #58


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9069112..1194c1c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Shorthand command for `minecraft-mod-manager`; you can now use `mcman` or `mmm` instead 🙂
 
+### Changed
+
+- Show a message why a mod wasn't installed if no versions were available [#58](https://github.com/Senth/minecraft-mod-manager/issues/58)
+
 ### Fixed
 
 - Mod information is now loaded properly even if it contains character errors [#60](https://github.com/Senth/minecraft-mod-manager/issues/60)
diff --git a/minecraft_mod_manager/adapters/repo_impl.py b/minecraft_mod_manager/adapters/repo_impl.py
index 6c48270..2d8f4f2 100644
--- a/minecraft_mod_manager/adapters/repo_impl.py
+++ b/minecraft_mod_manager/adapters/repo_impl.py
@@ -9,7 +9,6 @@ from ..core.entities.mod import Mod
 from ..core.entities.sites import Sites
 from ..core.entities.version_info import VersionInfo
 from ..core.errors.mod_not_found_exception import ModNotFoundException
-from ..core.utils.latest_version_finder import LatestVersionFinder
 from ..gateways.api.curse_api import CurseApi
 from ..gateways.api.modrinth_api import ModrinthApi
 from ..gateways.downloader import Downloader
@@ -45,25 +44,23 @@ class RepoImpl(ConfigureRepo, UpdateRepo, InstallRepo, ShowRepo):
     def get_all_mods(self) -> Sequence[Mod]:
         return self.mods
 
-    def get_latest_version(self, mod: Mod) -> Union[VersionInfo, None]:
+    def get_versions(self, mod: Mod) -> List[VersionInfo]:
         versions: List[VersionInfo] = []
 
         # Modrinth
-        if mod.site == Sites.modrinth or mod.site == Sites.unknown:
+        if mod.matches_site(Sites.modrinth):
             try:
                 Logger.verbose("🔍 Searching on Modrinth...", indent=1)
                 versions.extend(self.modrinth_api.get_all_versions(mod))
-                mod.site = Sites.modrinth
                 RepoImpl._print_found()
             except ModNotFoundException:
                 RepoImpl._print_not_found()
 
         # Curse
-        if mod.site == Sites.curse or mod.site == Sites.unknown:
+        if mod.matches_site(Sites.curse):
             try:
                 Logger.verbose("🔍 Searching on CurseForge...", indent=1)
                 versions.extend(self.curse_api.get_all_versions(mod))
-                mod.site = Sites.curse
                 RepoImpl._print_found()
             except ModNotFoundException:
                 RepoImpl._print_not_found()
@@ -71,7 +68,7 @@ class RepoImpl(ConfigureRepo, UpdateRepo, InstallRepo, ShowRepo):
         if len(versions) == 0:
             raise ModNotFoundException(mod)
 
-        return LatestVersionFinder.find_latest_version(mod, versions)
+        return versions
 
     def download(self, url: str, filename: str = "") -> Path:
         return Path(self.downloader.download(url, filename))
diff --git a/minecraft_mod_manager/app/download/download.py b/minecraft_mod_manager/app/download/download.py
index 680f6a2..471e97b 100644
--- a/minecraft_mod_manager/app/download/download.py
+++ b/minecraft_mod_manager/app/download/download.py
@@ -1,5 +1,7 @@
 from typing import List, Sequence
 
+from minecraft_mod_manager.core.utils.latest_version_finder import LatestVersionFinder
+
 from ...core.entities.mod import Mod, ModArg
 from ...core.entities.version_info import VersionInfo
 from ...core.errors.mod_not_found_exception import ModNotFoundException
@@ -23,9 +25,8 @@ class DownloadInfo:
 
 
 class Download:
-    def __init__(self, repo: DownloadRepo, success_prefix: str):
+    def __init__(self, repo: DownloadRepo):
         self._repo = repo
-        self._success_prefix = success_prefix
 
     def find_download_and_install(self, mods: Sequence[Mod]) -> None:
         mods_not_found: List[ModNotFoundException] = []
@@ -34,19 +35,17 @@ class Download:
         for mod in mods:
             try:
                 Logger.info(mod.id, LogColors.bold)
-                latest_version = self._repo.get_latest_version(mod)
+                versions = self._repo.get_versions(mod)
+                latest_version = LatestVersionFinder.find_latest_version(mod, versions, filter=True)
 
                 if latest_version:
                     download_info = DownloadInfo(mod, latest_version)
                     Logger.verbose("⬇ Downloading...", indent=1)
                     self._download_and_install(download_info)
-                    Logger.info(
-                        f"🟢 {self._success_prefix} -> {download_info.version_info.filename}",
-                        LogColors.green,
-                        indent=1,
-                    )
+                    # TODO #32 read mod again to get version number etc.
+                    self.on_version_found(download_info)
                 else:
-                    Logger.verbose("🟨 No new version found", LogColors.skip, indent=1)
+                    self.on_version_not_found(mod, versions)
 
             except ModNotFoundException as exception:
                 Logger.info("🔺 Mod not found on any site...", LogColors.red, indent=1)
@@ -59,6 +58,12 @@ class Download:
                 errorMessage += str(error) + "\n\n"
             Logger.info(errorMessage, LogColors.error)
 
+    def on_version_found(self, download_info: DownloadInfo) -> None:
+        raise NotImplementedError("Not implemented in subclass")
+
+    def on_version_not_found(self, mod: Mod, versions: List[VersionInfo]) -> None:
+        raise NotImplementedError("Not implemented in subclass")
+
     def _download_and_install(self, download_info: DownloadInfo) -> None:
         downloaded_mod = self._download(download_info.mod, download_info.version_info)
         self._repo.update_mod(downloaded_mod)
diff --git a/minecraft_mod_manager/app/download/download_repo.py b/minecraft_mod_manager/app/download/download_repo.py
index 9df8cdc..39550fe 100644
--- a/minecraft_mod_manager/app/download/download_repo.py
+++ b/minecraft_mod_manager/app/download/download_repo.py
@@ -1,11 +1,12 @@
 from pathlib import Path
+from typing import List
 
 from ...core.entities.mod import Mod
 from ...core.entities.version_info import VersionInfo
 
 
 class DownloadRepo:
-    def get_latest_version(self, mod: Mod) -> VersionInfo:
+    def get_versions(self, mod: Mod) -> List[VersionInfo]:
         raise NotImplementedError()
 
     def download(self, url: str, filename: str = "") -> Path:
diff --git a/minecraft_mod_manager/app/install/install.py b/minecraft_mod_manager/app/install/install.py
index 729319b..a234b53 100644
--- a/minecraft_mod_manager/app/install/install.py
+++ b/minecraft_mod_manager/app/install/install.py
@@ -1,15 +1,19 @@
+from datetime import datetime
 from typing import Dict, List, Sequence
 
+from ...config import config
 from ...core.entities.mod import Mod, ModArg
 from ...core.entities.mod_loaders import ModLoaders
+from ...core.entities.version_info import VersionInfo
+from ...core.utils.latest_version_finder import LatestVersionFinder
 from ...utils.logger import LogColors, Logger
-from ..download.download import Download
+from ..download.download import Download, DownloadInfo
 from .install_repo import InstallRepo
 
 
 class Install(Download):
     def __init__(self, repo: InstallRepo) -> None:
-        super().__init__(repo, "Installed")
+        super().__init__(repo)
         self._install_repo = repo
 
     def execute(self, mods: Sequence[ModArg]) -> None:
@@ -61,3 +65,48 @@ class Install(Download):
                 loader_max = ModLoaders.unknown
 
         return loader_max
+
+    def on_version_found(self, download_info: DownloadInfo) -> None:
+        # TODO #32 improve message
+        Logger.info(
+            f"🟢 Installed ({download_info.version_info.filename})",
+            LogColors.green,
+            indent=1,
+        )
+
+    def on_version_not_found(self, mod: Mod, versions: List[VersionInfo]) -> None:
+        Logger.info(f"🟨 All versions were filtered out", LogColors.skip, indent=1)
+
+        latest_unfiltered = LatestVersionFinder.find_latest_version(mod, versions, filter=False)
+        if latest_unfiltered:
+            Install._print_latest_unfiltered(mod, latest_unfiltered)
+
+    @staticmethod
+    def _print_latest_unfiltered(mod: Mod, latest: VersionInfo) -> None:
+        if config.filter.version and config.filter.version not in latest.minecraft_versions:
+            Logger.info("The latest version was filtered out by minecraft version", indent=2)
+            Logger.info(f"Run without {LogColors.command}--minecraft-version{LogColors.no_color}, or")
+            Logger.info(
+                f"run with {LogColors.command}--minecraft-version VERSION{LogColors.no_color} to download it", indent=3
+            )
+            Logger.info(str(latest.minecraft_versions), indent=3)
+
+        if LatestVersionFinder.is_filtered_by_stability(latest):
+            Logger.info("The latest version was filtered out by stability", indent=2)
+            Logger.info(
+                f"Run with {LogColors.command}--{latest.stability.value}{LogColors.no_color} to download it", indent=3
+            )
+
+        if LatestVersionFinder.is_filtered_by_mod_loader(mod.mod_loader, latest):
+            Logger.info("The latest versios was filtered out by mod loader", indent=2)
+            Logger.info(
+                f"Run with {LogColors.command}--mod_loader {next(iter(latest.mod_loaders))}{LogColors.command} to download it",
+                indent=3,
+            )
+
+        Logger.verbose("Latest version", LogColors.bold, indent=2)
+        width = 20
+        Logger.verbose("Upload date:".ljust(width) + str(datetime.fromtimestamp(latest.upload_time)), indent=3)
+        Logger.verbose("Stability:".ljust(width) + latest.stability.value, indent=3)
+        Logger.verbose("Minecraft versions:".ljust(width) + str(latest.minecraft_versions), indent=3)
+        Logger.verbose("Filename:".ljust(width) + latest.filename, indent=3)
diff --git a/minecraft_mod_manager/app/update/update.py b/minecraft_mod_manager/app/update/update.py
index 23e28ea..c38884e 100644
--- a/minecraft_mod_manager/app/update/update.py
+++ b/minecraft_mod_manager/app/update/update.py
@@ -1,13 +1,15 @@
 from typing import List, Sequence
 
 from ...core.entities.mod import Mod, ModArg
-from ..download.download import Download
+from ...core.entities.version_info import VersionInfo
+from ...utils.logger import LogColors, Logger
+from ..download.download import Download, DownloadInfo
 from .update_repo import UpdateRepo
 
 
 class Update(Download):
     def __init__(self, repo: UpdateRepo) -> None:
-        super().__init__(repo, "Updated")
+        super().__init__(repo)
         self._update_repo = repo
 
     def execute(self, mods: Sequence[ModArg]) -> None:
@@ -23,3 +25,14 @@ class Update(Download):
                     mods_to_update.append(mod)
 
         self.find_download_and_install(mods_to_update)
+
+    def on_version_found(self, download_info: DownloadInfo) -> None:
+        # TODO #32 improve message
+        Logger.info(
+            f"🟢 Updated -> {download_info.version_info.filename}",
+            LogColors.green,
+            indent=1,
+        )
+
+    def on_version_not_found(self, mod: Mod, versions: List[VersionInfo]) -> None:
+        Logger.verbose("🟨 No new version found", LogColors.skip, indent=1)
diff --git a/minecraft_mod_manager/core/entities/mod.py b/minecraft_mod_manager/core/entities/mod.py
index 5182f8c..ddc01b5 100644
--- a/minecraft_mod_manager/core/entities/mod.py
+++ b/minecraft_mod_manager/core/entities/mod.py
@@ -18,6 +18,9 @@ class ModArg:
         self.site_slug = slug
         """Mod slug on the site"""
 
+    def matches_site(self, site: Sites) -> bool:
+        return self.site == site or self.site == Sites.unknown or self.site == Sites.all
+
     def __str__(self) -> str:
         return f"{self.site.value}:{self.id}={self.site_slug}"
 
diff --git a/minecraft_mod_manager/core/entities/sites.py b/minecraft_mod_manager/core/entities/sites.py
index ee06e19..20b877a 100644
--- a/minecraft_mod_manager/core/entities/sites.py
+++ b/minecraft_mod_manager/core/entities/sites.py
@@ -7,6 +7,7 @@ class Sites(Enum):
     unknown = "unknown"
     curse = "curse"
     modrinth = "modrinth"
+    all = "all"
 
     @staticmethod
     def from_name(name: str) -> Sites:
diff --git a/minecraft_mod_manager/core/errors/mod_not_found_exception.py b/minecraft_mod_manager/core/errors/mod_not_found_exception.py
index c091037..b5e1866 100644
--- a/minecraft_mod_manager/core/errors/mod_not_found_exception.py
+++ b/minecraft_mod_manager/core/errors/mod_not_found_exception.py
@@ -10,14 +10,14 @@ class ModNotFoundException(Exception):
 
     def __str__(self) -> str:
         mod_name = self.mod.id
-        mod_repo = "any site"
+        mod_site = "any site"
 
         if self.mod.site != Sites.unknown:
             mod_name = self.mod.site_slug
-            mod_repo = self.mod.site.value
+            mod_site = self.mod.site.value
 
         return (
-            f"Mod {mod_name} not found on {mod_repo}.\n"
+            f"Mod {mod_name} not found on {mod_site}.\n"
             + "Check so that it's name is correct. Or you can set the name by running:\n"
             + f"{LogColors.command}{config.app_name} configure {self.mod.id}=NEW_NAME{LogColors.no_color}"
         )


Test patch:
diff --git a/minecraft_mod_manager/adapters/repo_impl_test.py b/minecraft_mod_manager/adapters/repo_impl_test.py
index fc44ebe..ab9497b 100644
--- a/minecraft_mod_manager/adapters/repo_impl_test.py
+++ b/minecraft_mod_manager/adapters/repo_impl_test.py
@@ -1,4 +1,4 @@
-from typing import Any, List, Literal, Union
+from typing import Any, List, Union
 
 import pytest
 from mockito import mock, unstub, verifyStubbedInvocationsAreUsed, when
@@ -9,7 +9,6 @@ from ..core.entities.mod_loaders import ModLoaders
 from ..core.entities.sites import Sites
 from ..core.entities.version_info import Stabilities, VersionInfo
 from ..core.errors.mod_not_found_exception import ModNotFoundException
-from ..core.utils.latest_version_finder import LatestVersionFinder
 from ..gateways.downloader import Downloader
 from ..gateways.jar_parser import JarParser
 from ..gateways.sqlite import Sqlite
@@ -45,19 +44,15 @@ class TestGetLatestVersion:
         self,
         name: str,
         mod: Mod,
-        expected_result: Union[VersionInfo, type, None],
-        expected_mod: Mod,
+        expected_result: Union[List[VersionInfo], type, None],
         curse_api_returns: Union[List[VersionInfo], ModNotFoundException, None] = None,
         modrinth_api_returns: Union[List[VersionInfo], ModNotFoundException, None] = None,
-        version_finder_returns: Union[VersionInfo, Literal["None"], None] = None,
     ) -> None:
         self.name = name
         self.mod = mod
         self.expected_result = expected_result
-        self.expected_mod = expected_mod
         self.curse_api_returns = curse_api_returns
         self.modrinth_api_returns = modrinth_api_returns
-        self.version_finder_returns = version_finder_returns
 
 
 def mod(site: Sites = Sites.unknown) -> Mod:
@@ -78,35 +73,56 @@ def version(site: Sites = Sites.unknown) -> VersionInfo:
 @pytest.mark.parametrize(
     "test",
     [
+        (
+            TestGetLatestVersion(
+                name="Get mod from Modrinth API when site type is specified",
+                mod=mod(site=Sites.modrinth),
+                modrinth_api_returns=[version(site=Sites.modrinth)],
+                expected_result=[version(site=Sites.modrinth)],
+            )
+        ),
         (
             TestGetLatestVersion(
                 name="Get mod from Modrinth API when site type is unspecified",
                 mod=mod(),
                 modrinth_api_returns=[version(site=Sites.modrinth)],
-                version_finder_returns=version(site=Sites.modrinth),
-                expected_result=version(site=Sites.modrinth),
-                expected_mod=mod(site=Sites.modrinth),
+                curse_api_returns=[],
+                expected_result=[version(site=Sites.modrinth)],
             )
         ),
         (
             TestGetLatestVersion(
-                name="Get mod from Curse API when site type is curse",
-                mod=mod(site=Sites.modrinth),
+                name="Get mod from Modrinth API when site type is unspecified and Curse returns Exception",
+                mod=mod(),
                 modrinth_api_returns=[version(site=Sites.modrinth)],
-                version_finder_returns=version(site=Sites.modrinth),
-                expected_result=version(site=Sites.modrinth),
-                expected_mod=mod(site=Sites.modrinth),
+                curse_api_returns=ModNotFoundException(mod()),
+                expected_result=[version(site=Sites.modrinth)],
+            )
+        ),
+        (
+            TestGetLatestVersion(
+                name="Get mod from Curse API when site type is curse",
+                mod=mod(site=Sites.curse),
+                curse_api_returns=[version(site=Sites.curse)],
+                expected_result=[version(site=Sites.curse)],
             )
         ),
         (
             TestGetLatestVersion(
                 name="Get mod from Curse API when site type is unspecified",
                 mod=mod(),
+                modrinth_api_returns=[],
+                curse_api_returns=[version(site=Sites.curse)],
+                expected_result=[version(site=Sites.curse)],
+            )
+        ),
+        (
+            TestGetLatestVersion(
+                name="Get mod from Curse API when site type is unspecified and Modrinth return exception",
+                mod=mod(),
                 modrinth_api_returns=ModNotFoundException(mod()),
                 curse_api_returns=[version(site=Sites.curse)],
-                version_finder_returns=version(site=Sites.curse),
-                expected_result=version(site=Sites.curse),
-                expected_mod=mod(site=Sites.curse),
+                expected_result=[version(site=Sites.curse)],
             )
         ),
         (
@@ -114,42 +130,25 @@ def version(site: Sites = Sites.unknown) -> VersionInfo:
                 name="Get mod from Curse API when site type is curse",
                 mod=mod(site=Sites.curse),
                 curse_api_returns=[version(site=Sites.curse)],
-                version_finder_returns=version(site=Sites.curse),
-                expected_result=version(site=Sites.curse),
-                expected_mod=mod(site=Sites.curse),
+                expected_result=[version(site=Sites.curse)],
             )
         ),
-        # FIXME Ability to download from both sites
-        # (
-        #     TestGetLatestVersion(
-        #         name="Don't specify mod site when returning mods from all sites",
-        #         mod=mod(),
-        #         modrinth_api_returns=[version(site=Sites.modrinth)],
-        #         curse_api_returns=[version(site=Sites.curse)],
-        #         version_finder_returns=version(site=Sites.curse),
-        #         expected_result=version(site=Sites.curse),
-        #         expected_mod=mod(),
-        #     )
-        # ),
         (
             TestGetLatestVersion(
-                name="Mod not found in any API",
+                name="Get mod from all sites",
                 mod=mod(),
-                modrinth_api_returns=ModNotFoundException(mod()),
-                curse_api_returns=ModNotFoundException(mod()),
-                expected_result=ModNotFoundException,
-                expected_mod=mod(),
+                modrinth_api_returns=[version(site=Sites.modrinth)],
+                curse_api_returns=[version(site=Sites.curse)],
+                expected_result=[version(site=Sites.modrinth), version(site=Sites.curse)],
             )
         ),
         (
             TestGetLatestVersion(
-                name="No latest version found",
+                name="Mod not found in any API",
                 mod=mod(),
                 modrinth_api_returns=ModNotFoundException(mod()),
-                curse_api_returns=[version(site=Sites.curse)],
-                version_finder_returns="None",
-                expected_result=None,
-                expected_mod=mod(site=Sites.curse),
+                curse_api_returns=ModNotFoundException(mod()),
+                expected_result=ModNotFoundException,
             )
         ),
     ],
@@ -161,32 +160,22 @@ def test_get_latest_version(test: TestGetLatestVersion, repo_impl: RepoImpl):
     mock_get_all_versions(repo_impl.modrinth_api, test.modrinth_api_returns)
     mock_get_all_versions(repo_impl.curse_api, test.curse_api_returns)
 
-    # Mocks Version Finder
-    if test.version_finder_returns:
-        if test.version_finder_returns == "None":
-            when(LatestVersionFinder).find_latest_version(...)
-        else:
-            when(LatestVersionFinder).find_latest_version(...).thenReturn(test.version_finder_returns)
-
     try:
         if test.expected_result == ModNotFoundException:
             with pytest.raises(ModNotFoundException) as e:
-                repo_impl.get_latest_version(test.mod)
+                repo_impl.get_versions(test.mod)
             assert test.expected_result == e.type
         else:
-            result = repo_impl.get_latest_version(test.mod)
+            result = repo_impl.get_versions(test.mod)
             assert test.expected_result == result
 
-        assert test.expected_mod == test.mod
-
     finally:
         verifyStubbedInvocationsAreUsed()
         unstub()
 
 
 def mock_get_all_versions(api: Any, result: Union[List[VersionInfo], ModNotFoundException, None]) -> None:
-    if result:
-        if type(result) == list:
-            when(api).get_all_versions(...).thenReturn(result)
-        elif type(result) == ModNotFoundException:
-            when(api).get_all_versions(...).thenRaise(result)
+    if type(result) == list:
+        when(api).get_all_versions(...).thenReturn(result)
+    elif type(result) == ModNotFoundException:
+        when(api).get_all_versions(...).thenRaise(result)
diff --git a/minecraft_mod_manager/app/download/download_test.py b/minecraft_mod_manager/app/download/download_test.py
index e291c71..1d025c3 100644
--- a/minecraft_mod_manager/app/download/download_test.py
+++ b/minecraft_mod_manager/app/download/download_test.py
@@ -19,18 +19,19 @@ def mock_repo():
 def test_download_and_install_when_found(mock_repo):
     input = [Mod("found", "")]
     version_info = VersionInfo(
-        stability=Stabilities.beta,
+        stability=Stabilities.release,
         mod_loaders=set([ModLoaders.fabric]),
         site=Sites.curse,
         upload_time=0,
         minecraft_versions=[],
         download_url="",
     )
-    when(mock_repo).get_latest_version(...).thenReturn(version_info)
+    when(mock_repo).get_versions(...).thenReturn([version_info])
     when(mock_repo).download(...).thenReturn(Path("mod.jar"))
     when(mock_repo).update_mod(...)
 
-    download = Download(mock_repo, "")
+    download = Download(mock_repo)
+    when(download).on_version_found(...)
     download.find_download_and_install(input)
 
     verifyStubbedInvocationsAreUsed()
diff --git a/minecraft_mod_manager/core/utils/latest_version_finder.py b/minecraft_mod_manager/core/utils/latest_version_finder.py
index 6ee67a6..608559b 100644
--- a/minecraft_mod_manager/core/utils/latest_version_finder.py
+++ b/minecraft_mod_manager/core/utils/latest_version_finder.py
@@ -8,28 +8,28 @@ from ..entities.version_info import Stabilities, VersionInfo
 
 class LatestVersionFinder:
     @staticmethod
-    def find_latest_version(mod: Mod, versions: List[VersionInfo]) -> Union[VersionInfo, None]:
+    def find_latest_version(mod: Mod, versions: List[VersionInfo], filter: bool) -> Union[VersionInfo, None]:
         latest_version: Union[VersionInfo, None] = None
 
         for version in versions:
-            if not LatestVersionFinder._is_filtered(mod, version):
+            if not filter or not LatestVersionFinder.is_filtered(mod, version):
                 if not latest_version or version.upload_time > latest_version.upload_time:
                     latest_version = version
 
         return latest_version
 
     @staticmethod
-    def _is_filtered(mod: Mod, version: VersionInfo) -> bool:
-        if LatestVersionFinder._is_filtered_by_stability(version):
+    def is_filtered(mod: Mod, version: VersionInfo) -> bool:
+        if LatestVersionFinder.is_filtered_by_stability(version):
             return True
-        if LatestVersionFinder._is_filtered_by_mc_version(version):
+        if LatestVersionFinder.is_filtered_by_mc_version(version):
             return True
-        if LatestVersionFinder._is_filtered_by_mod_loader(mod.mod_loader, version):
+        if LatestVersionFinder.is_filtered_by_mod_loader(mod.mod_loader, version):
             return True
         return False
 
     @staticmethod
-    def _is_filtered_by_stability(version: VersionInfo) -> bool:
+    def is_filtered_by_stability(version: VersionInfo) -> bool:
         if config.filter.stability == Stabilities.alpha:
             return False
         elif config.filter.stability == Stabilities.beta:
@@ -39,13 +39,13 @@ class LatestVersionFinder:
         return False
 
     @staticmethod
-    def _is_filtered_by_mc_version(version: VersionInfo) -> bool:
+    def is_filtered_by_mc_version(version: VersionInfo) -> bool:
         if config.filter.version:
             return config.filter.version not in version.minecraft_versions
         return False
 
     @staticmethod
-    def _is_filtered_by_mod_loader(prev: ModLoaders, version: VersionInfo) -> bool:
+    def is_filtered_by_mod_loader(prev: ModLoaders, version: VersionInfo) -> bool:
         """Check whether this version should be filtered by mod_loader.
         Will check both if a global filter has been set, otherwise it will match against
         the already install version's mod loader (if one exists).
diff --git a/minecraft_mod_manager/core/utils/latest_version_finder_test.py b/minecraft_mod_manager/core/utils/latest_version_finder_test.py
index 2394ff3..ec5c7eb 100644
--- a/minecraft_mod_manager/core/utils/latest_version_finder_test.py
+++ b/minecraft_mod_manager/core/utils/latest_version_finder_test.py
@@ -325,6 +325,20 @@ def reset_filter() -> None:
 def test_find_latest_version(test: Test):
     print(test.name)
     set_filter(test.filter)
-    result = LatestVersionFinder.find_latest_version(test.mod, test.versions)
+    result = LatestVersionFinder.find_latest_version(test.mod, test.versions, filter=True)
     reset_filter()
     assert test.expected == result
+
+
+def test_find_latest_version_unfiltered():
+    set_filter(Filter(loader=ModLoaders.forge, version="1.16.5"))
+    versions = [
+        version_info(uploaded=10, mod_loaders=[ModLoaders.forge], versions=["1.16.4"]),
+        version_info(uploaded=20, mod_loaders=[ModLoaders.fabric], versions=["1.16.5"]),
+        version_info(uploaded=40, mod_loaders=[ModLoaders.fabric]),
+        version_info(uploaded=30, mod_loaders=[ModLoaders.fabric], versions=["1.16.2", "1.16.5"]),
+    ]
+    expected = version_info(uploaded=40, mod_loaders=[ModLoaders.fabric])
+    result = LatestVersionFinder.find_latest_version(mod(), versions, filter=False)
+    reset_filter()
+    assert expected == result