diff --git a/docs/ahriman.application.handlers.rst b/docs/ahriman.application.handlers.rst index 4cf98974..342280a6 100644 --- a/docs/ahriman.application.handlers.rst +++ b/docs/ahriman.application.handlers.rst @@ -92,6 +92,14 @@ ahriman.application.handlers.patch module :no-undoc-members: :show-inheritance: +ahriman.application.handlers.pkgbuild module +-------------------------------------------- + +.. automodule:: ahriman.application.handlers.pkgbuild + :members: + :no-undoc-members: + :show-inheritance: + ahriman.application.handlers.rebuild module ------------------------------------------- diff --git a/docs/ahriman.core.formatters.rst b/docs/ahriman.core.formatters.rst index 85f7b329..36f23094 100644 --- a/docs/ahriman.core.formatters.rst +++ b/docs/ahriman.core.formatters.rst @@ -76,6 +76,14 @@ ahriman.core.formatters.patch\_printer module :no-undoc-members: :show-inheritance: +ahriman.core.formatters.pkgbuild\_printer module +------------------------------------------------ + +.. automodule:: ahriman.core.formatters.pkgbuild_printer + :members: + :no-undoc-members: + :show-inheritance: + ahriman.core.formatters.printer module -------------------------------------- diff --git a/src/ahriman/application/handlers/change.py b/src/ahriman/application/handlers/change.py index 2dcf7e8e..89396ce9 100644 --- a/src/ahriman/application/handlers/change.py +++ b/src/ahriman/application/handlers/change.py @@ -54,7 +54,7 @@ class Change(Handler): case Action.List: changes = client.package_changes_get(args.package) ChangesPrinter(changes)(verbose=True, separator="") - Change.check_status(args.exit_code, not changes.is_empty) + Change.check_status(args.exit_code, changes.changes is not None) case Action.Remove: client.package_changes_update(args.package, Changes()) diff --git a/src/ahriman/application/handlers/pkgbuild.py b/src/ahriman/application/handlers/pkgbuild.py new file mode 100644 index 00000000..9f42244f --- /dev/null +++ b/src/ahriman/application/handlers/pkgbuild.py @@ -0,0 +1,101 @@ +# +# 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 . +# +import argparse + +from dataclasses import replace + +from ahriman.application.application import Application +from ahriman.application.handlers.handler import Handler, SubParserAction +from ahriman.core.configuration import Configuration +from ahriman.core.formatters import PkgbuildPrinter +from ahriman.models.action import Action +from ahriman.models.repository_id import RepositoryId + + +class Pkgbuild(Handler): + """ + package pkgbuild handler + """ + + ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io + + @classmethod + def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, + report: bool) -> None: + """ + callback for command line + + Args: + args(argparse.Namespace): command line args + repository_id(RepositoryId): repository unique identifier + configuration(Configuration): configuration instance + report(bool): force enable or disable reporting + """ + application = Application(repository_id, configuration, report=True) + client = application.repository.reporter + + match args.action: + case Action.List: + changes = client.package_changes_get(args.package) + PkgbuildPrinter(changes)(verbose=True, separator="") + Pkgbuild.check_status(args.exit_code, changes.pkgbuild is not None) + case Action.Remove: + changes = client.package_changes_get(args.package) + client.package_changes_update(args.package, replace(changes, pkgbuild=None)) + + @staticmethod + def _set_package_pkgbuild_parser(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for package pkgbuild subcommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("package-pkgbuild", help="get package pkgbuild", + description="retrieve package PKGBUILD stored in database", + epilog="This command requests package status from the web interface " + "if it is available.") + parser.add_argument("package", help="package base") + parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", + action="store_true") + parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True) + return parser + + @staticmethod + def _set_package_pkgbuild_remove_parser(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for package pkgbuild remove subcommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("package-pkgbuild-remove", help="remove package pkgbuild", + description="remove the package PKGBUILD stored remotely") + parser.add_argument("package", help="package base") + parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True) + return parser + + arguments = [_set_package_pkgbuild_parser, _set_package_pkgbuild_remove_parser] diff --git a/src/ahriman/core/formatters/__init__.py b/src/ahriman/core/formatters/__init__.py index 1b4f8bf9..55bf6995 100644 --- a/src/ahriman/core/formatters/__init__.py +++ b/src/ahriman/core/formatters/__init__.py @@ -26,6 +26,7 @@ from ahriman.core.formatters.event_stats_printer import EventStatsPrinter from ahriman.core.formatters.package_printer import PackagePrinter from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter from ahriman.core.formatters.patch_printer import PatchPrinter +from ahriman.core.formatters.pkgbuild_printer import PkgbuildPrinter from ahriman.core.formatters.printer import Printer from ahriman.core.formatters.repository_printer import RepositoryPrinter from ahriman.core.formatters.repository_stats_printer import RepositoryStatsPrinter diff --git a/src/ahriman/core/formatters/changes_printer.py b/src/ahriman/core/formatters/changes_printer.py index 9fba4fa8..e28da064 100644 --- a/src/ahriman/core/formatters/changes_printer.py +++ b/src/ahriman/core/formatters/changes_printer.py @@ -45,7 +45,7 @@ class ChangesPrinter(Printer): Returns: list[Property]: list of content properties """ - if self.changes.is_empty: + if self.changes.changes is None: return [] return [Property("", self.changes.changes, is_required=True, indent=0)] @@ -57,6 +57,6 @@ class ChangesPrinter(Printer): Returns: str | None: content title if it can be generated and ``None`` otherwise """ - if self.changes.is_empty: + if self.changes.changes is None: return None return self.changes.last_commit_sha diff --git a/src/ahriman/core/formatters/pkgbuild_printer.py b/src/ahriman/core/formatters/pkgbuild_printer.py new file mode 100644 index 00000000..d16ffc83 --- /dev/null +++ b/src/ahriman/core/formatters/pkgbuild_printer.py @@ -0,0 +1,62 @@ +# +# 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 ahriman.core.formatters.printer import Printer +from ahriman.models.changes import Changes +from ahriman.models.property import Property + + +class PkgbuildPrinter(Printer): + """ + print content of the pkgbuild stored in changes + + Attributes: + changes(Changes): package changes + """ + + def __init__(self, changes: Changes) -> None: + """ + Args: + changes(Changes): package changes + """ + Printer.__init__(self) + self.changes = changes + + def properties(self) -> list[Property]: + """ + convert content into printable data + + Returns: + list[Property]: list of content properties + """ + if self.changes.pkgbuild is None: + return [] + return [Property("", self.changes.pkgbuild, is_required=True, indent=0)] + + # pylint: disable=redundant-returns-doc + def title(self) -> str | None: + """ + generate entry title from content + + Returns: + str | None: content title if it can be generated and ``None`` otherwise + """ + if self.changes.pkgbuild is None: + return None + return self.changes.last_commit_sha diff --git a/src/ahriman/models/changes.py b/src/ahriman/models/changes.py index 423f8a89..7b08f2e3 100644 --- a/src/ahriman/models/changes.py +++ b/src/ahriman/models/changes.py @@ -38,16 +38,6 @@ class Changes: changes: str | None = None pkgbuild: str | None = None - @property - def is_empty(self) -> bool: - """ - validate that changes are not empty - - Returns: - bool: ``True`` in case if changes are not set and ``False`` otherwise - """ - return self.changes is None - @classmethod def from_json(cls, dump: dict[str, Any]) -> Self: """ diff --git a/tests/ahriman/application/handlers/test_handler_pkgbuild.py b/tests/ahriman/application/handlers/test_handler_pkgbuild.py new file mode 100644 index 00000000..d6a2b8bd --- /dev/null +++ b/tests/ahriman/application/handlers/test_handler_pkgbuild.py @@ -0,0 +1,100 @@ +import argparse +import pytest + +from pytest_mock import MockerFixture + +from ahriman.application.handlers.pkgbuild import Pkgbuild +from ahriman.core.configuration import Configuration +from ahriman.core.database import SQLite +from ahriman.core.repository import Repository +from ahriman.models.action import Action +from ahriman.models.changes import Changes + + +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + """ + default arguments for these test cases + + Args: + args(argparse.Namespace): command line arguments fixture + + Returns: + argparse.Namespace: generated arguments for these test cases + """ + args.action = Action.List + args.exit_code = False + args.package = "package" + return args + + +def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository, + mocker: MockerFixture) -> None: + """ + must run command + """ + args = _default_args(args) + mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) + application_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", + return_value=Changes("sha", "change", "pkgbuild content")) + check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status") + print_mock = mocker.patch("ahriman.core.formatters.Printer.print") + + _, repository_id = configuration.check_loaded() + Pkgbuild.run(args, repository_id, configuration, report=False) + application_mock.assert_called_once_with(args.package) + check_mock.assert_called_once_with(False, True) + print_mock.assert_called_once_with(verbose=True, log_fn=pytest.helpers.anyvar(int), separator="") + + +def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository, + mocker: MockerFixture) -> None: + """ + must raise ExitCode exception on empty pkgbuild result + """ + args = _default_args(args) + args.exit_code = True + mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) + mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=Changes()) + check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status") + + _, repository_id = configuration.check_loaded() + Pkgbuild.run(args, repository_id, configuration, report=False) + check_mock.assert_called_once_with(True, False) + + +def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository, + mocker: MockerFixture) -> None: + """ + must remove package pkgbuild + """ + args = _default_args(args) + args.action = Action.Remove + mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) + changes = Changes("sha", "change", "pkgbuild content") + mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes) + update_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update") + + _, repository_id = configuration.check_loaded() + Pkgbuild.run(args, repository_id, configuration, report=False) + update_mock.assert_called_once_with(args.package, Changes("sha", "change", None)) + + +def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, database: SQLite, + mocker: MockerFixture) -> None: + """ + must create application object with native reporting + """ + args = _default_args(args) + mocker.patch("ahriman.core.database.SQLite.load", return_value=database) + load_mock = mocker.patch("ahriman.core.repository.Repository.load") + + _, repository_id = configuration.check_loaded() + Pkgbuild.run(args, repository_id, configuration, report=False) + load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0) + + +def test_disallow_multi_architecture_run() -> None: + """ + must not allow multi architecture run + """ + assert not Pkgbuild.ALLOW_MULTI_ARCHITECTURE_RUN diff --git a/tests/ahriman/core/formatters/conftest.py b/tests/ahriman/core/formatters/conftest.py index 4f76a69d..6e993d65 100644 --- a/tests/ahriman/core/formatters/conftest.py +++ b/tests/ahriman/core/formatters/conftest.py @@ -2,24 +2,26 @@ import pytest from pathlib import Path -from ahriman.core.formatters import \ - AurPrinter, \ - ChangesPrinter, \ - ConfigurationPathsPrinter, \ - ConfigurationPrinter, \ - EventStatsPrinter, \ - PackagePrinter, \ - PackageStatsPrinter, \ - PatchPrinter, \ - RepositoryPrinter, \ - RepositoryStatsPrinter, \ - StatusPrinter, \ - StringPrinter, \ - TreePrinter, \ - UpdatePrinter, \ - UserPrinter, \ - ValidationPrinter, \ +from ahriman.core.formatters import ( + AurPrinter, + ChangesPrinter, + ConfigurationPathsPrinter, + ConfigurationPrinter, + EventStatsPrinter, + PackagePrinter, + PackageStatsPrinter, + PatchPrinter, + PkgbuildPrinter, + RepositoryPrinter, + RepositoryStatsPrinter, + StatusPrinter, + StringPrinter, + TreePrinter, + UpdatePrinter, + UserPrinter, + ValidationPrinter, VersionPrinter +) from ahriman.models.aur_package import AURPackage from ahriman.models.build_status import BuildStatus from ahriman.models.changes import Changes @@ -55,6 +57,17 @@ def changes_printer() -> ChangesPrinter: return ChangesPrinter(Changes("sha", "changes")) +@pytest.fixture +def pkgbuild_printer() -> PkgbuildPrinter: + """ + fixture for pkgbuild printer + + Returns: + PkgbuildPrinter: pkgbuild printer test instance + """ + return PkgbuildPrinter(Changes("sha", "changes", "pkgbuild content")) + + @pytest.fixture def configuration_paths_printer() -> ConfigurationPathsPrinter: """ diff --git a/tests/ahriman/core/formatters/test_pkgbuild_printer.py b/tests/ahriman/core/formatters/test_pkgbuild_printer.py new file mode 100644 index 00000000..db3a3b5a --- /dev/null +++ b/tests/ahriman/core/formatters/test_pkgbuild_printer.py @@ -0,0 +1,32 @@ +from ahriman.core.formatters import PkgbuildPrinter +from ahriman.models.changes import Changes + + +def test_properties(pkgbuild_printer: PkgbuildPrinter) -> None: + """ + must return non-empty properties list + """ + assert pkgbuild_printer.properties() + + +def test_properties_empty() -> None: + """ + must return empty properties list if pkgbuild is empty + """ + assert not PkgbuildPrinter(Changes()).properties() + assert not PkgbuildPrinter(Changes("sha", "changes")).properties() + + +def test_title(pkgbuild_printer: PkgbuildPrinter) -> None: + """ + must return non-empty title + """ + assert pkgbuild_printer.title() + + +def test_title_empty() -> None: + """ + must return empty title if change is empty + """ + assert not PkgbuildPrinter(Changes()).title() + assert not PkgbuildPrinter(Changes("sha")).title() diff --git a/tests/ahriman/models/test_changes.py b/tests/ahriman/models/test_changes.py index decfd429..8eb6e919 100644 --- a/tests/ahriman/models/test_changes.py +++ b/tests/ahriman/models/test_changes.py @@ -1,17 +1,6 @@ from ahriman.models.changes import Changes -def test_is_empty() -> None: - """ - must check if changes are empty - """ - assert Changes().is_empty - assert Changes("sha").is_empty - - assert not Changes("sha", "change").is_empty - assert not Changes(None, "change").is_empty # well, ok - - def test_changes_from_json_view() -> None: """ must construct same object from json