refactor: lightweight Package.from_archive method

This commit is contained in:
2026-03-11 17:11:37 +02:00
parent 2cd4ef5e86
commit 81aeb56ba3
20 changed files with 223 additions and 139 deletions

View File

@@ -28,6 +28,14 @@ ahriman.core.alpm.pacman\_database module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.core.alpm.pacman\_handle module
---------------------------------------
.. automodule:: ahriman.core.alpm.pacman_handle
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.alpm.pkgbuild\_parser module ahriman.core.alpm.pkgbuild\_parser module
----------------------------------------- -----------------------------------------

View File

@@ -24,10 +24,11 @@ import tarfile
from collections.abc import Iterable, Iterator from collections.abc import Iterable, Iterator
from functools import cached_property from functools import cached_property
from pathlib import Path from pathlib import Path
from pyalpm import DB, Handle, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found] from pyalpm import DB, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
from string import Template from string import Template
from ahriman.core.alpm.pacman_database import PacmanDatabase from ahriman.core.alpm.pacman_database import PacmanDatabase
from ahriman.core.alpm.pacman_handle import PacmanHandle
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.core.utils import trim_package from ahriman.core.utils import trim_package
@@ -61,16 +62,16 @@ class Pacman(LazyLogging):
self.refresh_database = refresh_database self.refresh_database = refresh_database
@cached_property @cached_property
def handle(self) -> Handle: def handle(self) -> PacmanHandle:
""" """
pyalpm handle pyalpm handle
Returns: Returns:
Handle: generated pyalpm handle instance PacmanHandle: generated pyalpm handle instance
""" """
return self.__create_handle(refresh_database=self.refresh_database) return self.__create_handle(refresh_database=self.refresh_database)
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> Handle: def __create_handle(self, *, refresh_database: PacmanSynchronization) -> PacmanHandle:
""" """
create lazy handle function create lazy handle function
@@ -78,14 +79,14 @@ class Pacman(LazyLogging):
refresh_database(PacmanSynchronization): synchronize local cache to remote refresh_database(PacmanSynchronization): synchronize local cache to remote
Returns: Returns:
Handle: fully initialized pacman handle PacmanHandle: fully initialized pacman handle
""" """
pacman_root = self.configuration.getpath("alpm", "database") pacman_root = self.configuration.getpath("alpm", "database")
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache") use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
root = self.configuration.getpath("alpm", "root") root = self.configuration.getpath("alpm", "root")
handle = Handle(str(root), str(database_path)) handle = PacmanHandle(str(root), str(database_path))
for repository in self.configuration.getlist("alpm", "repositories"): for repository in self.configuration.getlist("alpm", "repositories"):
database = self.database_init(handle, repository, self.repository_id.architecture) database = self.database_init(handle, repository, self.repository_id.architecture)
@@ -99,12 +100,12 @@ class Pacman(LazyLogging):
return handle return handle
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None: def database_copy(self, handle: PacmanHandle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
""" """
copy database from the operating system root to the ahriman local home copy database from the operating system root to the ahriman local home
Args: Args:
handle(Handle): pacman handle which will be used for database copying handle(PacmanHandle): pacman handle which will be used for database copying
database(DB): pacman database instance to be copied database(DB): pacman database instance to be copied
pacman_root(Path): operating system pacman root pacman_root(Path): operating system pacman root
use_ahriman_cache(bool): use local ahriman cache instead of system one use_ahriman_cache(bool): use local ahriman cache instead of system one
@@ -133,12 +134,12 @@ class Pacman(LazyLogging):
with self.repository_paths.preserve_owner(): with self.repository_paths.preserve_owner():
shutil.copy(src, dst) shutil.copy(src, dst)
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB: def database_init(self, handle: PacmanHandle, repository: str, architecture: str) -> DB:
""" """
create database instance from pacman handler and set its properties create database instance from pacman handler and set its properties
Args: Args:
handle(Handle): pacman handle which will be used for database initializing handle(PacmanHandle): pacman handle which will be used for database initializing
repository(str): pacman repository name (e.g. core) repository(str): pacman repository name (e.g. core)
architecture(str): repository architecture architecture(str): repository architecture
@@ -164,12 +165,12 @@ class Pacman(LazyLogging):
return database return database
def database_sync(self, handle: Handle, *, force: bool) -> None: def database_sync(self, handle: PacmanHandle, *, force: bool) -> None:
""" """
sync local database sync local database
Args: Args:
handle(Handle): pacman handle which will be used for database sync handle(PacmanHandle): pacman handle which will be used for database sync
force(bool): force database synchronization (same as ``pacman -Syy``) force(bool): force database synchronization (same as ``pacman -Syy``)
""" """
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force) self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)

View File

@@ -0,0 +1,81 @@
#
# 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 <http://www.gnu.org/licenses/>.
#
from pathlib import Path
from pyalpm import Handle, Package # type: ignore[import-not-found]
from tempfile import TemporaryDirectory
from typing import Any, ClassVar, Self
class PacmanHandle:
"""
lightweight wrapper for pacman handle to be used for direct alpm operations (e.g. package load)
Attributes:
handle(Handle): pyalpm handle instance
"""
_ephemeral: ClassVar[Self | None] = None
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
Args:
*args(Any): positional arguments for :class:`pyalpm.Handle`
**kwargs(Any): keyword arguments for :class:`pyalpm.Handle`
"""
self.handle = Handle(*args, **kwargs)
@classmethod
def ephemeral(cls) -> Self:
"""
create temporary instance with no access to real databases
Returns:
Self: loaded class
"""
if cls._ephemeral is None:
# handle creates alpm version file, but we don't use it
# so it is ok to just remove it
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
cls._ephemeral = cls("/", dir_name)
return cls._ephemeral
def package_load(self, path: Path) -> Package:
"""
load package from path to the archive
Args:
path(Path): path to package archive
Returns:
Package: package instance
"""
return self.handle.load_pkg(str(path))
def __getattr__(self, item: str) -> Any:
"""
proxy methods for :class:`pyalpm.Handle`, because it doesn't allow subclassing
Args:
item(str): property name
Returns:
Any: attribute by its name
"""
return self.handle.__getattribute__(item)

View File

@@ -19,11 +19,9 @@
# #
from sqlite3 import Connection from sqlite3 import Connection
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.utils import package_like from ahriman.core.utils import package_like
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
__all__ = ["migrate_data", "steps"] __all__ = ["migrate_data", "steps"]
@@ -61,12 +59,9 @@ def migrate_package_depends(connection: Connection, configuration: Configuration
if not configuration.repository_paths.repository.is_dir(): if not configuration.repository_paths.repository.is_dir():
return return
_, repository_id = configuration.check_loaded()
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
package_list = [] package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
base = Package.from_archive(full_path, pacman) base = Package.from_archive(full_path)
for package, description in base.packages.items(): for package, description in base.packages.items():
package_list.append({ package_list.append({
"make_depends": description.make_depends, "make_depends": description.make_depends,

View File

@@ -19,11 +19,9 @@
# #
from sqlite3 import Connection from sqlite3 import Connection
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.utils import package_like from ahriman.core.utils import package_like
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
__all__ = ["migrate_data", "steps"] __all__ = ["migrate_data", "steps"]
@@ -58,12 +56,9 @@ def migrate_package_check_depends(connection: Connection, configuration: Configu
if not configuration.repository_paths.repository.is_dir(): if not configuration.repository_paths.repository.is_dir():
return return
_, repository_id = configuration.check_loaded()
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
package_list = [] package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
base = Package.from_archive(full_path, pacman) base = Package.from_archive(full_path)
for package, description in base.packages.items(): for package, description in base.packages.items():
package_list.append({ package_list.append({
"check_depends": description.check_depends, "check_depends": description.check_depends,

View File

@@ -19,11 +19,9 @@
# #
from sqlite3 import Connection from sqlite3 import Connection
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.utils import package_like from ahriman.core.utils import package_like
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
__all__ = ["migrate_data", "steps"] __all__ = ["migrate_data", "steps"]
@@ -64,12 +62,9 @@ def migrate_package_base_packager(connection: Connection, configuration: Configu
if not configuration.repository_paths.repository.is_dir(): if not configuration.repository_paths.repository.is_dir():
return return
_, repository_id = configuration.check_loaded()
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
package_list = [] package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()): for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
package = Package.from_archive(full_path, pacman) package = Package.from_archive(full_path)
package_list.append({ package_list.append({
"package_base": package.base, "package_base": package.base,
"packager": package.packager, "packager": package.packager,

View File

@@ -20,13 +20,11 @@
from dataclasses import replace from dataclasses import replace
from sqlite3 import Connection from sqlite3 import Connection
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.repository import Explorer from ahriman.core.repository import Explorer
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
from ahriman.core.utils import atomic_move, package_like, symlink_relative from ahriman.core.utils import atomic_move, package_like, symlink_relative
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@@ -45,29 +43,27 @@ def migrate_data(connection: Connection, configuration: Configuration) -> None:
for repository_id in Explorer.repositories_extract(configuration): for repository_id in Explorer.repositories_extract(configuration):
paths = replace(configuration.repository_paths, repository_id=repository_id) paths = replace(configuration.repository_paths, repository_id=repository_id)
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
# create archive directory if required # create archive directory if required
if not paths.archive.is_dir(): if not paths.archive.is_dir():
with paths.preserve_owner(): with paths.preserve_owner():
paths.archive.mkdir(mode=0o755, parents=True) paths.archive.mkdir(mode=0o755, parents=True)
move_packages(paths, pacman) move_packages(paths)
def move_packages(repository_paths: RepositoryPaths, pacman: Pacman) -> None: def move_packages(repository_paths: RepositoryPaths) -> None:
""" """
move packages from repository to archive and create symbolic links move packages from repository to archive and create symbolic links
Args: Args:
repository_paths(RepositoryPaths): repository paths instance repository_paths(RepositoryPaths): repository paths instance
pacman(Pacman): alpm wrapper instance
""" """
for archive in filter(package_like, repository_paths.repository.iterdir()): for archive in filter(package_like, repository_paths.repository.iterdir()):
if not archive.is_file(follow_symlinks=False): if not archive.is_file(follow_symlinks=False):
continue # skip symbolic links if any continue # skip symbolic links if any
package = Package.from_archive(archive, pacman) package = Package.from_archive(archive)
artifacts = [archive] artifacts = [archive]
# check if there are signatures for this package and append it here too # check if there are signatures for this package and append it here too
if (signature := GPG.signature(archive)).exists(): if (signature := GPG.signature(archive)).exists():

View File

@@ -57,7 +57,7 @@ class Executor(PackageInfo, Cleaner):
for path in filter(package_like, archive.iterdir()): for path in filter(package_like, archive.iterdir()):
# check if package version is the same # check if package version is the same
built = Package.from_archive(path, self.pacman) built = Package.from_archive(path)
if built.version != package.version: if built.version != package.version:
continue continue
@@ -117,7 +117,7 @@ class Executor(PackageInfo, Cleaner):
else: else:
built = task.build(path, PACKAGER=packager) built = task.build(path, PACKAGER=packager)
package.with_packages(built, self.pacman) package.with_packages(built)
for src in built: for src in built:
dst = self.paths.packages / src.name dst = self.paths.packages / src.name
atomic_move(src, dst) atomic_move(src, dst)

View File

@@ -24,19 +24,31 @@ from functools import cmp_to_key
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.build_tools.package_version import PackageVersion from ahriman.core.build_tools.package_version import PackageVersion
from ahriman.core.build_tools.sources import Sources from ahriman.core.build_tools.sources import Sources
from ahriman.core.repository.repository_properties import RepositoryProperties from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.core.status import Client
from ahriman.core.utils import package_like from ahriman.core.utils import package_like
from ahriman.models.changes import Changes from ahriman.models.changes import Changes
from ahriman.models.package import Package from ahriman.models.package import Package
class PackageInfo(RepositoryProperties): class PackageInfo(LazyLogging):
""" """
handler for the package information handler for the package information
Attributes:
configuration(Configuration): configuration instance
pacman(Pacman): alpm wrapper instance
reporter(Client): build status reporter instance
""" """
configuration: Configuration
pacman: Pacman
reporter: Client
def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]: def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]:
""" """
generate full dependencies list including transitive dependencies generate full dependencies list including transitive dependencies
@@ -87,7 +99,7 @@ class PackageInfo(RepositoryProperties):
# we are iterating over bases, not single packages # we are iterating over bases, not single packages
for full_path in packages: for full_path in packages:
try: try:
local = Package.from_archive(full_path, self.pacman) local = Package.from_archive(full_path)
if (source := sources.get(local.base)) is not None: # update source with remote if (source := sources.get(local.base)) is not None: # update source with remote
local.remote = source local.remote = source
@@ -115,10 +127,12 @@ class PackageInfo(RepositoryProperties):
Returns: Returns:
list[Package]: list of packages belonging to this base, sorted by version by ascension list[Package]: list of packages belonging to this base, sorted by version by ascension
""" """
paths = self.configuration.repository_paths
packages: dict[tuple[str, str], Package] = {} packages: dict[tuple[str, str], Package] = {}
# we can't use here load_archives, because it ignores versions # 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()): for full_path in filter(package_like, paths.archive_for(package_base).iterdir()):
local = Package.from_archive(full_path, self.pacman) local = Package.from_archive(full_path)
packages.setdefault((local.base, local.version), local).packages.update(local.packages) packages.setdefault((local.base, local.version), local).packages.update(local.packages)
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version) comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
@@ -138,7 +152,7 @@ class PackageInfo(RepositoryProperties):
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name: with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
dir_path = Path(dir_name) dir_path = Path(dir_name)
patches = self.reporter.package_patches_get(package.base, None) patches = self.reporter.package_patches_get(package.base, None)
current_commit_sha = Sources.load(dir_path, package, patches, self.paths) current_commit_sha = Sources.load(dir_path, package, patches, self.configuration.repository_paths)
if current_commit_sha != last_commit_sha: if current_commit_sha != last_commit_sha:
return Sources.changes(dir_path, last_commit_sha) return Sources.changes(dir_path, last_commit_sha)
@@ -154,7 +168,7 @@ class PackageInfo(RepositoryProperties):
Returns: Returns:
list[Package]: list of packages properties list[Package]: list of packages properties
""" """
packages = self.load_archives(filter(package_like, self.paths.repository.iterdir())) packages = self.load_archives(filter(package_like, self.configuration.repository_paths.repository.iterdir()))
if filter_packages: if filter_packages:
packages = [package for package in packages if package.base in filter_packages] packages = [package for package in packages if package.base in filter_packages]
@@ -167,7 +181,7 @@ class PackageInfo(RepositoryProperties):
Returns: Returns:
list[Path]: list of filenames from the directory list[Path]: list of filenames from the directory
""" """
return list(filter(package_like, self.paths.packages.iterdir())) return list(filter(package_like, self.configuration.repository_paths.packages.iterdir()))
def packages_depend_on(self, packages: list[Package], depends_on: Iterable[str] | None) -> list[Package]: def packages_depend_on(self, packages: list[Package], depends_on: Iterable[str] | None) -> list[Package]:
""" """

View File

@@ -26,6 +26,7 @@ from pyalpm import vercmp # type: ignore[import-not-found]
from typing import Any, Self from typing import Any, Self
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.pacman_handle import PacmanHandle
from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
from ahriman.core.utils import dataclass_view, full_version, list_flatmap, parse_version, srcinfo_property_list from ahriman.core.utils import dataclass_view, full_version, list_flatmap, parse_version, srcinfo_property_list
@@ -186,18 +187,17 @@ class Package(LazyLogging):
return sorted(packages) return sorted(packages)
@classmethod @classmethod
def from_archive(cls, path: Path, pacman: Pacman) -> Self: def from_archive(cls, path: Path) -> Self:
""" """
construct package properties from package archive construct package properties from package archive
Args: Args:
path(Path): path to package archive path(Path): path to package archive
pacman(Pacman): alpm wrapper instance
Returns: Returns:
Self: package properties Self: package properties
""" """
package = pacman.handle.load_pkg(str(path)) package = PacmanHandle.ephemeral().package_load(path)
description = PackageDescription.from_package(package, path) description = PackageDescription.from_package(package, path)
return cls( return cls(
base=package.base or package.name, base=package.base or package.name,
@@ -400,17 +400,16 @@ class Package(LazyLogging):
""" """
return dataclass_view(self) return dataclass_view(self)
def with_packages(self, packages: Iterable[Path], pacman: Pacman) -> None: def with_packages(self, packages: Iterable[Path]) -> None:
""" """
replace packages descriptions with ones from archives replace packages descriptions with ones from archives
Args: Args:
packages(Iterable[Path]): paths to package archives packages(Iterable[Path]): paths to package archives
pacman(Pacman): alpm wrapper instance
""" """
self.packages = {} # reset state self.packages = {} # reset state
for package in packages: for package in packages:
archive = self.from_archive(package, pacman) archive = self.from_archive(package)
if archive.base != self.base: if archive.base != self.base:
continue continue

View File

@@ -0,0 +1,37 @@
import pytest
from pathlib import Path
from unittest.mock import MagicMock
from ahriman.core.alpm.pacman_handle import PacmanHandle
def test_package_load() -> None:
"""
must load package from archive path
"""
local = Path("local")
instance = PacmanHandle.ephemeral()
handle_mock = instance.handle = MagicMock()
instance.package_load(local)
handle_mock.load_pkg.assert_called_once_with(str(local))
PacmanHandle._ephemeral = None
def test_getattr() -> None:
"""
must proxy attribute access to underlying handle
"""
instance = PacmanHandle.ephemeral()
assert instance.dbpath
def test_getattr_not_found() -> None:
"""
must raise AttributeError for missing handle attributes
"""
instance = PacmanHandle.ephemeral()
with pytest.raises(AttributeError):
assert instance.random_attribute

View File

@@ -34,8 +34,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman) package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
migrate_package_depends(connection, configuration) migrate_package_depends(connection, configuration)
package_mock.assert_called_once_with( package_mock.assert_called_once_with(package_ahriman.packages[package_ahriman.base].filepath)
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int))
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{ connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
"make_depends": package_ahriman.packages[package_ahriman.base].make_depends, "make_depends": package_ahriman.packages[package_ahriman.base].make_depends,
"opt_depends": package_ahriman.packages[package_ahriman.base].opt_depends, "opt_depends": package_ahriman.packages[package_ahriman.base].opt_depends,

View File

@@ -34,8 +34,7 @@ def test_migrate_package_depends(connection: Connection, configuration: Configur
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman) package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
migrate_package_check_depends(connection, configuration) migrate_package_check_depends(connection, configuration)
package_mock.assert_called_once_with( package_mock.assert_called_once_with(package_ahriman.packages[package_ahriman.base].filepath)
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int))
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{ connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
"check_depends": package_ahriman.packages[package_ahriman.base].check_depends, "check_depends": package_ahriman.packages[package_ahriman.base].check_depends,
"package": package_ahriman.base, "package": package_ahriman.base,

View File

@@ -34,8 +34,7 @@ def test_migrate_package_base_packager(connection: Connection, configuration: Co
package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman) package_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
migrate_package_base_packager(connection, configuration) migrate_package_base_packager(connection, configuration)
package_mock.assert_called_once_with( package_mock.assert_called_once_with(package_ahriman.packages[package_ahriman.base].filepath)
package_ahriman.packages[package_ahriman.base].filepath, pytest.helpers.anyvar(int))
connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{ connection.executemany.assert_called_once_with(pytest.helpers.anyvar(str, strict=True), [{
"package_base": package_ahriman.base, "package_base": package_ahriman.base,
"packager": package_ahriman.packager, "packager": package_ahriman.packager,

View File

@@ -7,7 +7,6 @@ from sqlite3 import Connection
from typing import Any 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.database.migrations.m016_archive import migrate_data, move_packages from ahriman.core.database.migrations.m016_archive import migrate_data, move_packages
from ahriman.models.package import Package from ahriman.models.package import Package
@@ -28,12 +27,12 @@ def test_migrate_data(connection: Connection, configuration: Configuration, mock
migrate_data(connection, configuration) migrate_data(connection, configuration)
migration_mock.assert_has_calls([ migration_mock.assert_has_calls([
MockCall(replace(configuration.repository_paths, repository_id=repository), pytest.helpers.anyvar(int)) MockCall(replace(configuration.repository_paths, repository_id=repository))
for repository in repositories for repository in repositories
]) ])
def test_move_packages(repository_paths: RepositoryPaths, pacman: Pacman, package_ahriman: Package, def test_move_packages(repository_paths: RepositoryPaths, package_ahriman: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must move packages to the archive directory must move packages to the archive directory
@@ -57,9 +56,9 @@ def test_move_packages(repository_paths: RepositoryPaths, pacman: Pacman, packag
move_mock = mocker.patch("ahriman.core.database.migrations.m016_archive.atomic_move") move_mock = mocker.patch("ahriman.core.database.migrations.m016_archive.atomic_move")
symlink_mock = mocker.patch("pathlib.Path.symlink_to") symlink_mock = mocker.patch("pathlib.Path.symlink_to")
move_packages(repository_paths, pacman) move_packages(repository_paths)
archive_mock.assert_has_calls([ archive_mock.assert_has_calls([
MockCall(repository_paths.repository / filename, pacman) MockCall(repository_paths.repository / filename)
for filename in ("file.pkg.tar.xz", "file2.pkg.tar.xz") for filename in ("file.pkg.tar.xz", "file2.pkg.tar.xz")
]) ])
move_mock.assert_has_calls([ move_mock.assert_has_calls([

View File

@@ -6,7 +6,6 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite from ahriman.core.database import SQLite
from ahriman.core.repository.cleaner import Cleaner from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.repository.executor import Executor from ahriman.core.repository.executor import Executor
from ahriman.core.repository.package_info import PackageInfo
from ahriman.core.repository.update_handler import UpdateHandler from ahriman.core.repository.update_handler import UpdateHandler
from ahriman.models.pacman_synchronization import PacmanSynchronization from ahriman.models.pacman_synchronization import PacmanSynchronization
@@ -50,23 +49,6 @@ def executor(configuration: Configuration, database: SQLite, mocker: MockerFixtu
refresh_pacman_database=PacmanSynchronization.Disabled) refresh_pacman_database=PacmanSynchronization.Disabled)
@pytest.fixture
def package_info(configuration: Configuration, database: SQLite) -> PackageInfo:
"""
fixture for package info
Args:
configuration(Configuration): configuration fixture
database(SQLite): database fixture
Returns:
PackageInfo: package info test instance
"""
_, repository_id = configuration.check_loaded()
return PackageInfo(repository_id, configuration, database, report=False,
refresh_pacman_database=PacmanSynchronization.Disabled)
@pytest.fixture @pytest.fixture
def update_handler(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> UpdateHandler: def update_handler(configuration: Configuration, database: SQLite, mocker: MockerFixture) -> UpdateHandler:
""" """

View File

@@ -118,7 +118,7 @@ def test_package_build(executor: Executor, package_ahriman: Package, mocker: Moc
init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None) init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None)
package_mock.assert_called_once_with(Path("local"), executor.architecture, None) package_mock.assert_called_once_with(Path("local"), executor.architecture, None)
lookup_mock.assert_called_once_with(package_ahriman) lookup_mock.assert_called_once_with(package_ahriman)
with_packages_mock.assert_called_once_with([Path(package_ahriman.base)], executor.pacman) with_packages_mock.assert_called_once_with([Path(package_ahriman.base)])
rename_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base) rename_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)

View File

@@ -4,12 +4,12 @@ from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import MagicMock from unittest.mock import MagicMock
from ahriman.core.repository.package_info import PackageInfo from ahriman.core.repository import Repository
from ahriman.models.changes import Changes from ahriman.models.changes import Changes
from ahriman.models.package import Package from ahriman.models.package import Package
def test_full_depends(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package, def test_full_depends(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
pyalpm_package_ahriman: MagicMock) -> None: pyalpm_package_ahriman: MagicMock) -> None:
""" """
must extract all dependencies from the package must extract all dependencies from the package
@@ -18,18 +18,18 @@ def test_full_depends(package_info: PackageInfo, package_ahriman: Package, packa
database_mock = MagicMock() database_mock = MagicMock()
database_mock.pkgcache = [pyalpm_package_ahriman] database_mock.pkgcache = [pyalpm_package_ahriman]
package_info.pacman = MagicMock() repository.pacman = MagicMock()
package_info.pacman.handle.get_syncdbs.return_value = [database_mock] repository.pacman.handle.get_syncdbs.return_value = [database_mock]
assert package_info.full_depends(package_ahriman, [package_python_schedule]) == package_ahriman.depends assert repository.full_depends(package_ahriman, [package_python_schedule]) == package_ahriman.depends
package_python_schedule.packages[package_python_schedule.base].depends = [package_ahriman.base] package_python_schedule.packages[package_python_schedule.base].depends = [package_ahriman.base]
expected = sorted(set(package_python_schedule.depends + package_ahriman.depends)) expected = sorted(set(package_python_schedule.depends + package_ahriman.depends))
assert package_info.full_depends(package_python_schedule, [package_python_schedule]) == expected assert repository.full_depends(package_python_schedule, [package_python_schedule]) == expected
def test_load_archives(package_ahriman: Package, package_python_schedule: Package, def test_load_archives(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
package_info: PackageInfo, mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must return all packages grouped by package base must return all packages grouped by package base
""" """
@@ -45,7 +45,7 @@ def test_load_archives(package_ahriman: Package, package_python_schedule: Packag
(package_ahriman, None), (package_ahriman, None),
]) ])
packages = package_info.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")]) packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")])
assert len(packages) == 2 assert len(packages) == 2
assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base} assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base}
@@ -56,22 +56,22 @@ def test_load_archives(package_ahriman: Package, package_python_schedule: Packag
assert set(archives) == expected assert set(archives) == expected
def test_load_archives_failed(package_info: PackageInfo, mocker: MockerFixture) -> None: def test_load_archives_failed(repository: Repository, mocker: MockerFixture) -> None:
""" """
must skip packages which cannot be loaded must skip packages which cannot be loaded
""" """
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=Exception) mocker.patch("ahriman.models.package.Package.from_archive", side_effect=Exception)
assert not package_info.load_archives([Path("a.pkg.tar.xz")]) assert not repository.load_archives([Path("a.pkg.tar.xz")])
def test_load_archives_not_package(package_info: PackageInfo) -> None: def test_load_archives_not_package(repository: Repository) -> None:
""" """
must skip not packages from iteration must skip not packages from iteration
""" """
assert not package_info.load_archives([Path("a.tar.xz")]) assert not repository.load_archives([Path("a.tar.xz")])
def test_load_archives_different_version(package_info: PackageInfo, package_python_schedule: Package, def test_load_archives_different_version(repository: Repository, package_python_schedule: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must load packages with different versions choosing maximal must load packages with different versions choosing maximal
@@ -86,12 +86,12 @@ def test_load_archives_different_version(package_info: PackageInfo, package_pyth
single_packages[0].version = "0.0.1-1" single_packages[0].version = "0.0.1-1"
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=single_packages) mocker.patch("ahriman.models.package.Package.from_archive", side_effect=single_packages)
packages = package_info.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")]) packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")])
assert len(packages) == 1 assert len(packages) == 1
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: def test_package_archives(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must load package archives sorted by version must load package archives sorted by version
""" """
@@ -110,12 +110,12 @@ def test_package_archives(package_info: PackageInfo, package_ahriman: Package, m
mocker.patch("pathlib.Path.iterdir", 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) mocker.patch("ahriman.models.package.Package.from_archive", side_effect=package)
result = package_info.package_archives(package_ahriman.base) result = repository.package_archives(package_ahriman.base)
assert len(result) == 5 assert len(result) == 5
assert [p.version for p in result] == [str(i) for i in range(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(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must load package changes must load package changes
""" """
@@ -123,23 +123,24 @@ def test_package_changes(package_info: PackageInfo, package_ahriman: Package, mo
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha2") load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha2")
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes", return_value=changes) changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes", return_value=changes)
assert package_info.package_changes(package_ahriman, changes.last_commit_sha) == changes assert repository.package_changes(package_ahriman, changes.last_commit_sha) == changes
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman, [], package_info.paths) load_mock.assert_called_once_with(
pytest.helpers.anyvar(int), package_ahriman, [], repository.configuration.repository_paths)
changes_mock.assert_called_once_with(pytest.helpers.anyvar(int), changes.last_commit_sha) changes_mock.assert_called_once_with(pytest.helpers.anyvar(int), changes.last_commit_sha)
def test_package_changes_skip(package_info: PackageInfo, package_ahriman: Package, mocker: MockerFixture) -> None: def test_package_changes_skip(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must skip loading package changes if no new commits must skip loading package changes if no new commits
""" """
mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha") mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha")
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes") changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes")
assert package_info.package_changes(package_ahriman, "sha") is None assert repository.package_changes(package_ahriman, "sha") is None
changes_mock.assert_not_called() changes_mock.assert_not_called()
def test_packages(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package, def test_packages(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must return repository packages must return repository packages
@@ -147,12 +148,12 @@ def test_packages(package_info: PackageInfo, package_ahriman: Package, package_p
mocker.patch("pathlib.Path.iterdir") mocker.patch("pathlib.Path.iterdir")
load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives", load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
return_value=[package_ahriman, package_python_schedule]) return_value=[package_ahriman, package_python_schedule])
assert package_info.packages() == [package_ahriman, package_python_schedule] assert repository.packages() == [package_ahriman, package_python_schedule]
# it uses filter object, so we cannot verify argument list =/ # it uses filter object, so we cannot verify argument list =/
load_mock.assert_called_once_with(pytest.helpers.anyvar(int)) load_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_packages_filter(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package, def test_packages_filter(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must filter result by bases must filter result by bases
@@ -160,33 +161,33 @@ def test_packages_filter(package_info: PackageInfo, package_ahriman: Package, pa
mocker.patch("pathlib.Path.iterdir") mocker.patch("pathlib.Path.iterdir")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives", mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
return_value=[package_ahriman, package_python_schedule]) return_value=[package_ahriman, package_python_schedule])
assert package_info.packages([package_ahriman.base]) == [package_ahriman] assert repository.packages([package_ahriman.base]) == [package_ahriman]
def test_packages_built(package_info: PackageInfo, mocker: MockerFixture) -> None: def test_packages_built(repository: Repository, mocker: MockerFixture) -> None:
""" """
must return build packages must return build packages
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz"), Path("b.pkg.tar.xz")]) mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz"), Path("b.pkg.tar.xz")])
assert package_info.packages_built() == [Path("b.pkg.tar.xz")] assert repository.packages_built() == [Path("b.pkg.tar.xz")]
def test_packages_depend_on(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package, def test_packages_depend_on(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must filter packages by depends list must filter packages by depends list
""" """
mocker.patch("ahriman.core.repository.repository.Repository.packages", mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule]) return_value=[package_ahriman, package_python_schedule])
assert package_info.packages_depend_on([package_ahriman], {"python-srcinfo"}) == [package_ahriman] assert repository.packages_depend_on([package_ahriman], {"python-srcinfo"}) == [package_ahriman]
def test_packages_depend_on_empty(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package, def test_packages_depend_on_empty(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must return all packages in case if no filter is provided must return all packages in case if no filter is provided
""" """
mocker.patch("ahriman.core.repository.repository.Repository.packages", mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule]) return_value=[package_ahriman, package_python_schedule])
assert package_info.packages_depend_on([package_ahriman, package_python_schedule], None) == \ assert repository.packages_depend_on([package_ahriman, package_python_schedule], None) == \
[package_ahriman, package_python_schedule] [package_ahriman, package_python_schedule]

View File

@@ -89,22 +89,6 @@ def pkgbuild_ahriman(resource_path_root: Path) -> Pkgbuild:
return Pkgbuild.from_file(pkgbuild) return Pkgbuild.from_file(pkgbuild)
@pytest.fixture
def pyalpm_handle(pyalpm_package_ahriman: MagicMock) -> MagicMock:
"""
mock object for pyalpm
Args:
pyalpm_package_ahriman(MagicMock): mock object for pyalpm package
Returns:
MagicMock: pyalpm mock
"""
mock = MagicMock()
mock.handle.load_pkg.return_value = pyalpm_package_ahriman
return mock
@pytest.fixture @pytest.fixture
def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescription) -> MagicMock: def pyalpm_package_description_ahriman(package_description_ahriman: PackageDescription) -> MagicMock:
""" """

View File

@@ -148,13 +148,14 @@ def test_packages_full(package_ahriman: Package) -> None:
assert package_ahriman.packages_full == [package_ahriman.base, f"{package_ahriman.base}-git"] assert package_ahriman.packages_full == [package_ahriman.base, f"{package_ahriman.base}-git"]
def test_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: def test_from_archive(package_ahriman: Package, pyalpm_package_ahriman: MagicMock, mocker: MockerFixture) -> None:
""" """
must construct package from alpm library must construct package from alpm library
""" """
mocker.patch("ahriman.core.alpm.pacman_handle.PacmanHandle.package_load", return_value=pyalpm_package_ahriman)
mocker.patch("ahriman.models.package_description.PackageDescription.from_package", mocker.patch("ahriman.models.package_description.PackageDescription.from_package",
return_value=package_ahriman.packages[package_ahriman.base]) return_value=package_ahriman.packages[package_ahriman.base])
generated = Package.from_archive(Path("path"), pyalpm_handle) generated = Package.from_archive(Path("path"))
generated.remote = package_ahriman.remote generated.remote = package_ahriman.remote
assert generated == package_ahriman assert generated == package_ahriman
@@ -165,13 +166,12 @@ def test_from_archive_empty_base(package_ahriman: Package, pyalpm_package_ahrima
""" """
must construct package with empty base from alpm library must construct package with empty base from alpm library
""" """
pyalpm_handle = MagicMock()
type(pyalpm_package_ahriman).base = PropertyMock(return_value=None) type(pyalpm_package_ahriman).base = PropertyMock(return_value=None)
pyalpm_handle.handle.load_pkg.return_value = pyalpm_package_ahriman mocker.patch("ahriman.core.alpm.pacman_handle.PacmanHandle.package_load", return_value=pyalpm_package_ahriman)
mocker.patch("ahriman.models.package_description.PackageDescription.from_package", mocker.patch("ahriman.models.package_description.PackageDescription.from_package",
return_value=package_ahriman.packages[package_ahriman.base]) return_value=package_ahriman.packages[package_ahriman.base])
generated = Package.from_archive(Path("path"), pyalpm_handle) generated = Package.from_archive(Path("path"))
generated.remote = package_ahriman.remote generated.remote = package_ahriman.remote
assert generated == package_ahriman assert generated == package_ahriman
@@ -362,7 +362,7 @@ def test_vercmp(package_ahriman: Package, mocker: MockerFixture) -> None:
vercmp_mock.assert_called_once_with(package_ahriman.version, "version") vercmp_mock.assert_called_once_with(package_ahriman.version, "version")
def test_with_packages(package_ahriman: Package, package_python_schedule: Package, pacman: Pacman, def test_with_packages(package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None: mocker: MockerFixture) -> None:
""" """
must correctly replace packages descriptions must correctly replace packages descriptions
@@ -375,8 +375,8 @@ def test_with_packages(package_ahriman: Package, package_python_schedule: Packag
result = copy.deepcopy(package_ahriman) result = copy.deepcopy(package_ahriman)
package_ahriman.packages[package_ahriman.base].architecture = "i686" package_ahriman.packages[package_ahriman.base].architecture = "i686"
result.with_packages(paths, pacman) result.with_packages(paths)
from_archive_mock.assert_has_calls([ from_archive_mock.assert_has_calls([
MockCall(path, pacman) for path in paths MockCall(path) for path in paths
]) ])
assert result.packages[result.base] == package_ahriman.packages[package_ahriman.base] assert result.packages[result.base] == package_ahriman.packages[package_ahriman.base]