refactor: separate package_archives method from trigger

This commit is contained in:
2026-03-11 13:36:59 +02:00
parent 998ed48dde
commit 2cd4ef5e86
4 changed files with 66 additions and 47 deletions

View File

@@ -17,14 +17,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from collections.abc import Callable
from functools import cmp_to_key
from ahriman.core import context from ahriman.core import context
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.core.triggers import Trigger from ahriman.core.triggers import Trigger
from ahriman.core.utils import package_like
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
from ahriman.models.result import Result from ahriman.models.result import Result
@@ -78,27 +74,20 @@ class ArchiveRotationTrigger(Trigger):
""" """
return list(cls.CONFIGURATION_SCHEMA.keys()) return list(cls.CONFIGURATION_SCHEMA.keys())
def archives_remove(self, package: Package, pacman: Pacman) -> None: def archives_remove(self, package: Package, repository: Repository) -> None:
""" """
remove older versions of the specified package remove older versions of the specified package
Args: Args:
package(Package): package which has been updated to check for older versions package(Package): package which has been updated to check for older versions
pacman(Pacman): alpm wrapper instance repository(Repository): repository instance
""" """
# explicit guard to skip process in case if rotation is disabled # explicit guard to skip process in case if rotation is disabled
# this guard is supposed to speedup process # this guard is supposed to speedup process
if self.keep_built_packages == 0: if self.keep_built_packages == 0:
return return
packages: dict[tuple[str, str], Package] = {} to_remove = repository.package_archives(package.base)
# we can't use here load_archives, because it ignores versions
for full_path in filter(package_like, self.paths.archive_for(package.base).iterdir()):
local = Package.from_archive(full_path, pacman)
packages.setdefault((local.base, local.version), local).packages.update(local.packages)
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
to_remove = sorted(packages.values(), key=cmp_to_key(comparator))
for single in to_remove[:-self.keep_built_packages]: for single in to_remove[:-self.keep_built_packages]:
self.logger.info("removing version %s of package %s", single.version, single.base) self.logger.info("removing version %s of package %s", single.version, single.base)
@@ -115,7 +104,7 @@ class ArchiveRotationTrigger(Trigger):
packages(list[Package]): list of all available packages packages(list[Package]): list of all available packages
""" """
ctx = context.get() ctx = context.get()
pacman = ctx.get(Pacman) repository = ctx.get(Repository)
for package in result.success: for package in result.success:
self.archives_remove(package, pacman) self.archives_remove(package, repository)

View File

@@ -19,7 +19,8 @@
# #
import copy import copy
from collections.abc import Iterable from collections.abc import Callable, Iterable
from functools import cmp_to_key
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@@ -102,6 +103,27 @@ class PackageInfo(RepositoryProperties):
self.logger.exception("could not load package from %s", full_path) self.logger.exception("could not load package from %s", full_path)
return list(result.values()) return list(result.values())
def package_archives(self, package_base: str) -> list[Package]:
"""
load list of packages known for this package base. This method unlike
:func:`ahriman.core.repository.package_info.PackageInfo.load_archives` scans archive directory and loads all
versions available for the ``package_base``
Args:
package_base(str): package base
Returns:
list[Package]: list of packages belonging to this base, sorted by version by ascension
"""
packages: dict[tuple[str, str], Package] = {}
# we can't use here load_archives, because it ignores versions
for full_path in filter(package_like, self.paths.archive_for(package_base).iterdir()):
local = Package.from_archive(full_path, self.pacman)
packages.setdefault((local.base, local.version), local).packages.update(local.packages)
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
return sorted(packages.values(), key=cmp_to_key(comparator))
def package_changes(self, package: Package, last_commit_sha: str) -> Changes | None: def package_changes(self, package: Package, last_commit_sha: str) -> Changes | None:
""" """
extract package change for the package since last commit if available extract package change for the package since last commit if available

View File

@@ -3,12 +3,11 @@ import pytest
from dataclasses import replace from dataclasses import replace
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import call as MockCall from unittest.mock import call as MockCall
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.housekeeping import ArchiveRotationTrigger from ahriman.core.housekeeping import ArchiveRotationTrigger
from ahriman.core.repository import Repository
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
@@ -21,26 +20,24 @@ def test_configuration_sections(configuration: Configuration) -> None:
def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package, def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
pacman: Pacman, mocker: MockerFixture) -> None: repository: Repository, mocker: MockerFixture) -> None:
""" """
must remove older packages must remove older packages
""" """
def package(version: Any, *args: Any, **kwargs: Any) -> Package: packages = []
generated = replace(package_ahriman, version=str(version)) for i in range(5):
generated = replace(package_ahriman, version=str(i))
generated.packages = { generated.packages = {
key: replace(value, filename=str(version)) key: replace(value, filename=str(i))
for key, value in generated.packages.items() for key, value in generated.packages.items()
} }
return generated packages.append(generated)
mocker.patch("pathlib.Path.is_dir", return_value=True) mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=packages)
mocker.patch("ahriman.core.housekeeping.archive_rotation_trigger.package_like", return_value=True)
mocker.patch("pathlib.Path.glob", return_value=[Path(str(i)) for i in range(5)]) mocker.patch("pathlib.Path.glob", return_value=[Path(str(i)) for i in range(5)])
mocker.patch("pathlib.Path.iterdir", return_value=[Path(str(i)) for i in range(5)])
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=package)
unlink_mock = mocker.patch("pathlib.Path.unlink", autospec=True) unlink_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
archive_rotation_trigger.archives_remove(package_ahriman, pacman) archive_rotation_trigger.archives_remove(package_ahriman, repository)
unlink_mock.assert_has_calls([ unlink_mock.assert_has_calls([
MockCall(Path("0")), MockCall(Path("0")),
MockCall(Path("1")), MockCall(Path("1")),
@@ -48,28 +45,15 @@ def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, packa
def test_archives_remove_keep(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package, def test_archives_remove_keep(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
pacman: Pacman, mocker: MockerFixture) -> None: repository: Repository, mocker: MockerFixture) -> None:
""" """
must keep all packages if set to must keep all packages if set to
""" """
def package(version: Any, *args: Any, **kwargs: Any) -> Package: archives_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives")
generated = replace(package_ahriman, version=str(version))
generated.packages = {
key: replace(value, filename=str(version))
for key, value in generated.packages.items()
}
return generated
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("ahriman.core.housekeeping.archive_rotation_trigger.package_like", return_value=True)
mocker.patch("pathlib.Path.glob", return_value=[Path(str(i)) for i in range(5)])
mocker.patch("pathlib.Path.iterdir", return_value=[Path(str(i)) for i in range(5)])
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=package)
unlink_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
archive_rotation_trigger.keep_built_packages = 0 archive_rotation_trigger.keep_built_packages = 0
archive_rotation_trigger.archives_remove(package_ahriman, pacman) archive_rotation_trigger.archives_remove(package_ahriman, repository)
unlink_mock.assert_not_called() archives_mock.assert_not_called()
def test_on_result(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package, def test_on_result(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,

View File

@@ -91,6 +91,30 @@ def test_load_archives_different_version(package_info: PackageInfo, package_pyth
assert packages[0].version == package_python_schedule.version assert packages[0].version == package_python_schedule.version
def test_package_archives(package_info: PackageInfo, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load package archives sorted by version
"""
from dataclasses import replace
from typing import Any
def package(version: Any, *args: Any, **kwargs: Any) -> Package:
generated = replace(package_ahriman, version=str(version))
generated.packages = {
key: replace(value, filename=str(version))
for key, value in generated.packages.items()
}
return generated
mocker.patch("ahriman.core.repository.package_info.package_like", return_value=True)
mocker.patch("pathlib.Path.iterdir", return_value=[Path(str(i)) for i in range(5)])
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=package)
result = package_info.package_archives(package_ahriman.base)
assert len(result) == 5
assert [p.version for p in result] == [str(i) for i in range(5)]
def test_package_changes(package_info: PackageInfo, package_ahriman: Package, mocker: MockerFixture) -> None: def test_package_changes(package_info: PackageInfo, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must load package changes must load package changes