From 51431791df5f93b450bbe8733e1d3f83865fcb3d Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Sat, 14 Feb 2026 18:09:28 +0200 Subject: [PATCH] extract repository explorer to separate class --- src/ahriman/application/handlers/handler.py | 36 ++------ .../core/database/migrations/m016_archive.py | 9 +- src/ahriman/core/repository/__init__.py | 1 + src/ahriman/core/repository/explorer.py | 70 ++++++++++++++++ .../application/handlers/test_handler.py | 83 +++---------------- .../database/migrations/test_m016_archive.py | 2 +- .../ahriman/core/repository/test_explorer.py | 56 +++++++++++++ 7 files changed, 150 insertions(+), 107 deletions(-) create mode 100644 src/ahriman/core/repository/explorer.py create mode 100644 tests/ahriman/core/repository/test_explorer.py diff --git a/src/ahriman/application/handlers/handler.py b/src/ahriman/application/handlers/handler.py index e14fab43..75056e52 100644 --- a/src/ahriman/application/handlers/handler.py +++ b/src/ahriman/application/handlers/handler.py @@ -20,7 +20,7 @@ import argparse import logging -from collections.abc import Callable, Iterable +from collections.abc import Callable from multiprocessing import Pool from typing import ClassVar, TypeVar @@ -28,9 +28,9 @@ from ahriman.application.lock import Lock from ahriman.core.configuration import Configuration from ahriman.core.exceptions import ExitCode, MissingArchitectureError, MultipleArchitecturesError from ahriman.core.log.log_loader import LogLoader +from ahriman.core.repository import Explorer from ahriman.core.types import ExplicitBool from ahriman.models.repository_id import RepositoryId -from ahriman.models.repository_paths import RepositoryPaths # this workaround is for several things @@ -169,11 +169,6 @@ class Handler: Raises: MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed """ - configuration = Configuration() - configuration.load(args.configuration) - # pylint, wtf??? - root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return - # preparse systemd repository-id argument # we are using unescaped values, so / is not allowed here, because it is impossible to separate if from dashes if args.repository_id is not None: @@ -184,27 +179,10 @@ class Handler: if repository_parts: args.repository = "-".join(repository_parts) # replace slash with dash - # extract repository names first - if (from_args := args.repository) is not None: - repositories: Iterable[str] = [from_args] - elif from_filesystem := RepositoryPaths.known_repositories(root): - repositories = from_filesystem - else: # try to read configuration now - repositories = [configuration.get("repository", "name")] + configuration = Configuration() + configuration.load(args.configuration) + repositories = Explorer.repositories_extract(configuration, args.repository, args.architecture) - # extract architecture names - if (architecture := args.architecture) is not None: - parsed = set( - RepositoryId(architecture, repository) - for repository in repositories - ) - else: # try to read from file system - parsed = set( - RepositoryId(architecture, repository) - for repository in repositories - for architecture in RepositoryPaths.known_architectures(root, repository) - ) - - if not parsed: + if not repositories: raise MissingArchitectureError(args.command) - return sorted(parsed) + return sorted(repositories) diff --git a/src/ahriman/core/database/migrations/m016_archive.py b/src/ahriman/core/database/migrations/m016_archive.py index e2a5503d..65bf6b53 100644 --- a/src/ahriman/core/database/migrations/m016_archive.py +++ b/src/ahriman/core/database/migrations/m016_archive.py @@ -17,14 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import argparse - from dataclasses import replace from sqlite3 import Connection -from ahriman.application.handlers.handler import Handler from ahriman.core.alpm.pacman import Pacman from ahriman.core.configuration import Configuration +from ahriman.core.repository import Explorer from ahriman.core.sign.gpg import GPG from ahriman.core.utils import atomic_move, package_like, symlink_relative from ahriman.models.package import Package @@ -45,10 +43,7 @@ def migrate_data(connection: Connection, configuration: Configuration) -> None: """ del connection - config_path, _ = configuration.check_loaded() - args = argparse.Namespace(configuration=config_path, architecture=None, repository=None, repository_id=None) - - for repository_id in Handler.repositories_extract(args): + for repository_id in Explorer.repositories_extract(configuration): paths = replace(configuration.repository_paths, repository_id=repository_id) pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled) diff --git a/src/ahriman/core/repository/__init__.py b/src/ahriman/core/repository/__init__.py index 8b2c328d..d98b27b9 100644 --- a/src/ahriman/core/repository/__init__.py +++ b/src/ahriman/core/repository/__init__.py @@ -17,4 +17,5 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from ahriman.core.repository.explorer import Explorer from ahriman.core.repository.repository import Repository diff --git a/src/ahriman/core/repository/explorer.py b/src/ahriman/core/repository/explorer.py new file mode 100644 index 00000000..72de682f --- /dev/null +++ b/src/ahriman/core/repository/explorer.py @@ -0,0 +1,70 @@ +# +# Copyright (c) 2021-2026 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from collections.abc import Iterable + +from ahriman.core.configuration import Configuration +from ahriman.models.repository_id import RepositoryId +from ahriman.models.repository_paths import RepositoryPaths + + +class Explorer: + """ + helper to read filesystem and find created repositories + """ + + @staticmethod + def repositories_extract(configuration: Configuration, repository: str | None = None, + architecture: str | None = None) -> list[RepositoryId]: + """ + get known architectures + + Args: + configuration(Configuration): configuration instance + repository(str | None, optional): predefined repository name if available (Default value = None) + architecture(str | None, optional): predefined repository architecture if available (Default value = None) + + Returns: + list[RepositoryId]: list of repository names and architectures for which tree is created + """ + # pylint, wtf??? + root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return + + # extract repository names first + if repository is not None: + repositories: Iterable[str] = [repository] + elif from_filesystem := RepositoryPaths.known_repositories(root): + repositories = from_filesystem + else: # try to read configuration now + repositories = [configuration.get("repository", "name")] + + # extract architecture names + if architecture is not None: + parsed = set( + RepositoryId(architecture, repository) + for repository in repositories + ) + else: # try to read from file system + parsed = set( + RepositoryId(architecture, repository) + for repository in repositories + for architecture in RepositoryPaths.known_architectures(root, repository) + ) + + return sorted(parsed) diff --git a/tests/ahriman/application/handlers/test_handler.py b/tests/ahriman/application/handlers/test_handler.py index dc8001b2..52e8ff00 100644 --- a/tests/ahriman/application/handlers/test_handler.py +++ b/tests/ahriman/application/handlers/test_handler.py @@ -145,63 +145,11 @@ def test_repositories_extract(args: argparse.Namespace, configuration: Configura args.configuration = configuration.path args.repository = "repo" mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration)) - known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") - known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories") + extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract", + return_value=[RepositoryId("arch", "repo")]) assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")] - known_architectures_mock.assert_not_called() - known_repositories_mock.assert_not_called() - - -def test_repositories_extract_repository(args: argparse.Namespace, configuration: Configuration, - mocker: MockerFixture) -> None: - """ - must generate list of available repositories based on flags and tree - """ - args.architecture = "arch" - args.configuration = configuration.path - mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration)) - known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") - known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", - return_value={"repo"}) - - assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")] - known_architectures_mock.assert_not_called() - known_repositories_mock.assert_called_once_with(configuration.repository_paths.root) - - -def test_repositories_extract_repository_legacy(args: argparse.Namespace, configuration: Configuration, - mocker: MockerFixture) -> None: - """ - must generate list of available repositories based on flags and tree (legacy mode) - """ - args.architecture = "arch" - args.configuration = configuration.path - mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration)) - known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") - known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", - return_value=set()) - - assert Handler.repositories_extract(args) == [RepositoryId("arch", "aur")] - known_architectures_mock.assert_not_called() - known_repositories_mock.assert_called_once_with(configuration.repository_paths.root) - - -def test_repositories_extract_architecture(args: argparse.Namespace, configuration: Configuration, - mocker: MockerFixture) -> None: - """ - must read repository name from config - """ - args.configuration = configuration.path - args.repository = "repo" - mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration)) - known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", - return_value={"arch"}) - known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories") - - assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")] - known_architectures_mock.assert_called_once_with(configuration.repository_paths.root, "repo") - known_repositories_mock.assert_not_called() + extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), args.repository, args.architecture) def test_repositories_extract_empty(args: argparse.Namespace, configuration: Configuration, @@ -212,8 +160,7 @@ def test_repositories_extract_empty(args: argparse.Namespace, configuration: Con args.command = "config" args.configuration = configuration.path mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration)) - mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", return_value=set()) - mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", return_value=set()) + mocker.patch("ahriman.core.repository.Explorer.repositories_extract", return_value=[]) with pytest.raises(MissingArchitectureError): Handler.repositories_extract(args) @@ -227,12 +174,11 @@ def test_repositories_extract_systemd(args: argparse.Namespace, configuration: C args.configuration = configuration.path args.repository_id = "i686/some/repo/name" mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration)) - known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") - known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories") + extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract", + return_value=[RepositoryId("i686", "some-repo-name")]) assert Handler.repositories_extract(args) == [RepositoryId("i686", "some-repo-name")] - known_architectures_mock.assert_not_called() - known_repositories_mock.assert_not_called() + extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), "some-repo-name", "i686") def test_repositories_extract_systemd_with_dash(args: argparse.Namespace, configuration: Configuration, @@ -243,12 +189,11 @@ def test_repositories_extract_systemd_with_dash(args: argparse.Namespace, config args.configuration = configuration.path args.repository_id = "i686-some-repo-name" mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration)) - known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") - known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories") + extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract", + return_value=[RepositoryId("i686", "some-repo-name")]) assert Handler.repositories_extract(args) == [RepositoryId("i686", "some-repo-name")] - known_architectures_mock.assert_not_called() - known_repositories_mock.assert_not_called() + extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), "some-repo-name", "i686") def test_repositories_extract_systemd_legacy(args: argparse.Namespace, configuration: Configuration, @@ -259,10 +204,8 @@ def test_repositories_extract_systemd_legacy(args: argparse.Namespace, configura args.configuration = configuration.path args.repository_id = "i686" mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration)) - known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") - known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", - return_value=set()) + extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract", + return_value=[RepositoryId("i686", "aur")]) assert Handler.repositories_extract(args) == [RepositoryId("i686", "aur")] - known_architectures_mock.assert_not_called() - known_repositories_mock.assert_called_once_with(configuration.repository_paths.root) + extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), None, "i686") diff --git a/tests/ahriman/core/database/migrations/test_m016_archive.py b/tests/ahriman/core/database/migrations/test_m016_archive.py index 2c6745c5..962e4c7a 100644 --- a/tests/ahriman/core/database/migrations/test_m016_archive.py +++ b/tests/ahriman/core/database/migrations/test_m016_archive.py @@ -23,7 +23,7 @@ def test_migrate_data(connection: Connection, configuration: Configuration, mock repository_id, replace(repository_id, architecture="i686"), ] - mocker.patch("ahriman.application.handlers.handler.Handler.repositories_extract", return_value=repositories) + mocker.patch("ahriman.core.repository.Explorer.repositories_extract", return_value=repositories) migration_mock = mocker.patch("ahriman.core.database.migrations.m016_archive.move_packages") migrate_data(connection, configuration) diff --git a/tests/ahriman/core/repository/test_explorer.py b/tests/ahriman/core/repository/test_explorer.py new file mode 100644 index 00000000..5e71810e --- /dev/null +++ b/tests/ahriman/core/repository/test_explorer.py @@ -0,0 +1,56 @@ +from pytest_mock import MockerFixture + +from ahriman.core.configuration import Configuration +from ahriman.core.repository import Explorer +from ahriman.models.repository_id import RepositoryId + + +def test_repositories_extract(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must generate list of available repositories based on arguments + """ + known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") + known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories") + + assert Explorer.repositories_extract(configuration, "repo", "arch") == [RepositoryId("arch", "repo")] + known_architectures_mock.assert_not_called() + known_repositories_mock.assert_not_called() + + +def test_repositories_extract_repository(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must generate list of available repositories based on arguments and tree + """ + known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") + known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", + return_value={"repo"}) + + assert Explorer.repositories_extract(configuration, architecture="arch") == [RepositoryId("arch", "repo")] + known_architectures_mock.assert_not_called() + known_repositories_mock.assert_called_once_with(configuration.repository_paths.root) + + +def test_repositories_extract_repository_legacy(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must generate list of available repositories based on arguments and tree (legacy mode) + """ + known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures") + known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", + return_value=set()) + + assert Explorer.repositories_extract(configuration, architecture="arch") == [RepositoryId("arch", "aur")] + known_architectures_mock.assert_not_called() + known_repositories_mock.assert_called_once_with(configuration.repository_paths.root) + + +def test_repositories_extract_architecture(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must read repository name from config + """ + known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", + return_value={"arch"}) + known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories") + + assert Explorer.repositories_extract(configuration, repository="repo") == [RepositoryId("arch", "repo")] + known_architectures_mock.assert_called_once_with(configuration.repository_paths.root, "repo") + known_repositories_mock.assert_not_called()