support provides in aur

This commit is contained in:
2025-06-26 11:05:15 +03:00
parent 7f223ecc0a
commit 44241b09d2
10 changed files with 119 additions and 52 deletions

View File

@ -133,18 +133,18 @@ class Application(ApplicationPackages, ApplicationRepository):
if not process_dependencies or not packages:
return packages
def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]:
def missing_dependencies(sources: Iterable[Package]) -> dict[str, str | None]:
# append list of known packages with packages which are in current sources
satisfied_packages = known_packages | {
single
for package in source
for single in package.packages_full
for source in sources
for single in source.packages_full
}
return {
dependency: package.packager
for package in source
for dependency in package.depends_build
dependency: source.packager
for source in sources
for dependency in source.depends_build
if dependency not in satisfied_packages
}
@ -156,7 +156,7 @@ class Application(ApplicationPackages, ApplicationRepository):
# there is local cache, load package from it
leaf = Package.from_build(source_dir, self.repository.architecture, packager)
else:
leaf = Package.from_aur(package_name, packager)
leaf = Package.from_aur(package_name, packager, include_provides=True)
portion[leaf.base] = leaf
# register package in the database

View File

@ -223,22 +223,32 @@ class Pacman(LazyLogging):
return result
def package(self, package_name: str) -> Generator[Package, None, None]:
def package(self, package_name: str, include_provides: bool = False) -> Generator[Package, None, None]:
"""
retrieve list of the packages from the repository by name
retrieve list of the packages from the repository by name. If ``include_provides`` is set to ``True``, then
additionally this method will search through :attr:`alpm.Package.provides`; these packages will be returned
after exact match
Args:
package_name(str): package name to search
include_provides(bool, optional): search by provides if no exact match found (Default value = False)
Yields:
Package: list of packages which were returned by the query
"""
def is_package_provided(package: Package) -> bool:
return package_name in package.provides
for database in self.handle.get_syncdbs():
package = database.get_pkg(package_name)
if package is None:
continue
yield package
if include_provides:
for database in self.handle.get_syncdbs():
yield from filter(is_package_provided, database.search(package_name))
def packages(self) -> set[str]:
"""
get list of packages known for alpm

View File

@ -113,13 +113,17 @@ class AUR(Remote):
response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
return self.parse_response(response.json())
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None, include_provides: bool) -> AURPackage:
"""
get package info by its name
get package info by its name. If ``include_provides`` is set to ``True``, then, in addition, this method
will perform search by :attr:`ahriman.models.aur_package.AURPackage.provides` and return first package found.
Note, however, that in this case some implementation might not provide this method and search result will might
not be stable
Args:
package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
include_provides(bool): search by provides if no exact match found
Returns:
AURPackage: package which match the package name
@ -127,21 +131,32 @@ class AUR(Remote):
Raises:
UnknownPackageError: package doesn't exist
"""
def is_package_provided(package: AURPackage) -> bool:
return package_name in package.provides
packages = self.aur_request("info", package_name)
try:
return next(package for package in packages if package.name == package_name)
except StopIteration:
if include_provides:
provides = self.package_search(package_name, pacman=pacman, search_by="provides")
for stub in filter(is_package_provided, provides):
# return first found package
return self.package_info(stub.package_base, pacman=pacman, include_provides=False)
# either provides search is disabled or was still not found
raise UnknownPackageError(package_name) from None
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
def package_search(self, *keywords: str, pacman: Pacman | None, search_by: str | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
search_by(str | None): search by keywords
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return self.aur_request("search", *keywords, by="name-desc")
search_by = search_by or "name-desc"
return self.aur_request("search", *keywords, by=search_by)

View File

@ -107,13 +107,17 @@ class Official(Remote):
response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
return self.parse_response(response.json())
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None, include_provides: bool) -> AURPackage:
"""
get package info by its name
get package info by its name. If ``include_provides`` is set to ``True``, then, in addition, this method
will perform search by :attr:`ahriman.models.aur_package.AURPackage.provides` and return first package found.
Note, however, that in this case some implementation might not provide this method and search result will might
not be stable
Args:
package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
include_provides(bool): search by provides if no exact match found
Returns:
AURPackage: package which match the package name
@ -125,17 +129,20 @@ class Official(Remote):
try:
return next(package for package in packages if package.name == package_name)
except StopIteration:
# it does not support search by provides
raise UnknownPackageError(package_name) from None
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
def package_search(self, *keywords: str, pacman: Pacman | None, search_by: str | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
search_by(str | None): search by keywords
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return self.arch_request(*keywords, by="q")
search_by = search_by or "q"
return self.arch_request(*keywords, by=search_by)

View File

@ -38,13 +38,17 @@ class OfficialSyncdb(Official):
Still we leave search function based on the official repositories RPC.
"""
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None, include_provides: bool) -> AURPackage:
"""
get package info by its name
get package info by its name. If ``include_provides`` is set to ``True``, then, in addition, this method
will perform search by :attr:`ahriman.models.aur_package.AURPackage.provides` and return first package found.
Note, however, that in this case some implementation might not provide this method and search result will might
not be stable
Args:
package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
include_provides(bool): search by provides if no exact match found
Returns:
AURPackage: package which match the package name
@ -56,6 +60,6 @@ class OfficialSyncdb(Official):
raise UnknownPackageError(package_name)
try:
return next(AURPackage.from_pacman(package) for package in pacman.package(package_name))
return next(AURPackage.from_pacman(package) for package in pacman.package(package_name, include_provides))
except StopIteration:
raise UnknownPackageError(package_name) from None

View File

@ -41,22 +41,27 @@ class Remote(SyncHttpClient):
"""
@classmethod
def info(cls, package_name: str, *, pacman: Pacman | None = None) -> AURPackage:
def info(cls, package_name: str, *, pacman: Pacman | None = None, include_provides: bool = False) -> AURPackage:
"""
get package info by its name
get package info by its name. If ``include_provides`` is set to ``True``, then, in addition, this method
will perform search by :attr:`ahriman.models.aur_package.AURPackage.provides` and return first package found.
Note, however, that in this case some implementation might not provide this method and search result will might
not be stable
Args:
package_name(str): package name to search
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
include_provides(bool, optional): search by provides if no exact match found (Default value = False)
Returns:
AURPackage: package which match the package name
"""
return cls().package_info(package_name, pacman=pacman)
return cls().package_info(package_name, pacman=pacman, include_provides=include_provides)
@classmethod
def multisearch(cls, *keywords: str, pacman: Pacman | None = None) -> list[AURPackage]:
def multisearch(cls, *keywords: str, pacman: Pacman | None = None,
search_by: str | None = None) -> list[AURPackage]:
"""
search in remote repository by using API with multiple words. This method is required in order to handle
https://bugs.archlinux.org/task/49133. In addition, short words will be dropped
@ -65,6 +70,7 @@ class Remote(SyncHttpClient):
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
search_by(str | None, optional): search by keywords (Default value = None)
Returns:
list[AURPackage]: list of packages each of them matches all search terms
@ -72,7 +78,7 @@ class Remote(SyncHttpClient):
instance = cls()
packages: dict[str, AURPackage] = {}
for term in filter(lambda word: len(word) >= 3, keywords):
portion = instance.search(term, pacman=pacman)
portion = instance.package_search(term, pacman=pacman, search_by=search_by)
packages = {
package.name: package # not mistake to group them by name
for package in portion
@ -114,7 +120,7 @@ class Remote(SyncHttpClient):
raise NotImplementedError
@classmethod
def search(cls, *keywords: str, pacman: Pacman | None = None) -> list[AURPackage]:
def search(cls, *keywords: str, pacman: Pacman | None = None, search_by: str | None = None) -> list[AURPackage]:
"""
search package in AUR web
@ -122,19 +128,24 @@ class Remote(SyncHttpClient):
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
search_by(str | None, optional): search by keywords (Default value = None)
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return cls().package_search(*keywords, pacman=pacman)
return cls().package_search(*keywords, pacman=pacman, search_by=search_by)
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None, include_provides: bool) -> AURPackage:
"""
get package info by its name
get package info by its name. If ``include_provides`` is set to ``True``, then, in addition, this method
will perform search by :attr:`ahriman.models.aur_package.AURPackage.provides` and return first package found.
Note, however, that in this case some implementation might not provide this method and search result will might
not be stable
Args:
package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
include_provides(bool): search by provides if no exact match found
Returns:
AURPackage: package which match the package name
@ -144,13 +155,14 @@ class Remote(SyncHttpClient):
"""
raise NotImplementedError
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
def package_search(self, *keywords: str, pacman: Pacman | None, search_by: str | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
search_by(str | None): search by keywords
Returns:
list[AURPackage]: list of packages which match the criteria

View File

@ -213,18 +213,19 @@ class Package(LazyLogging):
)
@classmethod
def from_aur(cls, name: str, packager: str | None = None) -> Self:
def from_aur(cls, name: str, packager: str | None = None, *, include_provides: bool = False) -> Self:
"""
construct package properties from AUR page
Args:
name(str): package name (either base or normal name)
packager(str | None, optional): packager to be used for this build (Default value = None)
include_provides(bool, optional): search by provides if no exact match found (Default value = False)
Returns:
Self: package properties
"""
package = AUR.info(name)
package = AUR.info(name, include_provides=include_provides)
remote = RemoteSource(
source=PackageSource.AUR,
@ -310,7 +311,8 @@ class Package(LazyLogging):
)
@classmethod
def from_official(cls, name: str, pacman: Pacman, packager: str | None = None, *, use_syncdb: bool = True) -> Self:
def from_official(cls, name: str, pacman: Pacman, packager: str | None = None, *, use_syncdb: bool = True,
include_provides: bool = False) -> Self:
"""
construct package properties from official repository page
@ -319,11 +321,13 @@ class Package(LazyLogging):
pacman(Pacman): alpm wrapper instance
packager(str | None, optional): packager to be used for this build (Default value = None)
use_syncdb(bool, optional): use pacman databases instead of official repositories RPC (Default value = True)
include_provides(bool, optional): search by provides if no exact match found (Default value = False)
Returns:
Self: package properties
"""
package = OfficialSyncdb.info(name, pacman=pacman) if use_syncdb else Official.info(name)
impl = OfficialSyncdb if use_syncdb else Official
package = impl.info(name, pacman=pacman, include_provides=include_provides)
remote = RemoteSource(
source=PackageSource.Repository,

View File

@ -1,4 +1,6 @@
from pathlib import Path
from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import MagicMock, call as MockCall
from ahriman.application.application import Application
@ -73,6 +75,10 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
mock.packages_full = [package_base]
return mock
def get_package(name: str | Path, *args: Any, **kwargs: Any) -> Package:
name = name if isinstance(name, str) else name.name
return packages[name]
package_python_schedule.packages = {
package_python_schedule.base: package_python_schedule.packages[package_python_schedule.base]
}
@ -87,10 +93,8 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
}
mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=lambda p: p.name == "python")
package_aur_mock = mocker.patch("ahriman.models.package.Package.from_aur",
side_effect=lambda *args: packages[args[0]])
package_local_mock = mocker.patch("ahriman.models.package.Package.from_build",
side_effect=lambda *args: packages[args[0].name])
package_aur_mock = mocker.patch("ahriman.models.package.Package.from_aur", side_effect=get_package)
package_local_mock = mocker.patch("ahriman.models.package.Package.from_build", side_effect=get_package)
packages_mock = mocker.patch("ahriman.application.application.Application._known_packages",
return_value={"devtools", "python-build", "python-pytest"})
status_client_mock = mocker.patch("ahriman.core.status.Client.set_unknown")
@ -98,8 +102,8 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
result = application.with_dependencies([package_ahriman], process_dependencies=True)
assert {package.base: package for package in result} == packages
package_aur_mock.assert_has_calls([
MockCall(package_python_schedule.base, package_ahriman.packager),
MockCall("python-installer", package_ahriman.packager),
MockCall(package_python_schedule.base, package_ahriman.packager, include_provides=True),
MockCall("python-installer", package_ahriman.packager, include_provides=True),
], any_order=True)
package_local_mock.assert_has_calls([
MockCall(application.repository.paths.cache_for("python"), "x86_64", package_ahriman.packager),

View File

@ -13,8 +13,8 @@ def test_info(pacman: Pacman, mocker: MockerFixture) -> None:
must call info method
"""
info_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_info")
Remote.info("ahriman", pacman=pacman)
info_mock.assert_called_once_with("ahriman", pacman=pacman)
Remote.info("ahriman", pacman=pacman, include_provides=True)
info_mock.assert_called_once_with("ahriman", pacman=pacman, include_provides=True)
def test_multisearch(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
@ -22,10 +22,13 @@ def test_multisearch(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: Mo
must search in AUR with multiple words
"""
terms = ["ahriman", "is", "cool"]
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.search", return_value=[aur_package_ahriman])
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search", return_value=[aur_package_ahriman])
assert Remote.multisearch(*terms, pacman=pacman) == [aur_package_ahriman]
search_mock.assert_has_calls([MockCall("ahriman", pacman=pacman), MockCall("cool", pacman=pacman)])
assert Remote.multisearch(*terms, pacman=pacman, search_by="name") == [aur_package_ahriman]
search_mock.assert_has_calls([
MockCall("ahriman", pacman=pacman, search_by="name"),
MockCall("cool", pacman=pacman, search_by="name"),
])
def test_multisearch_empty(pacman: Pacman, mocker: MockerFixture) -> None:
@ -33,7 +36,7 @@ def test_multisearch_empty(pacman: Pacman, mocker: MockerFixture) -> None:
must return empty list if no long terms supplied
"""
terms = ["it", "is"]
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.search")
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search")
assert Remote.multisearch(*terms, pacman=pacman) == []
search_mock.assert_not_called()
@ -43,9 +46,9 @@ def test_multisearch_single(aur_package_ahriman: AURPackage, pacman: Pacman, moc
"""
must search in AUR with one word
"""
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.search", return_value=[aur_package_ahriman])
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search", return_value=[aur_package_ahriman])
assert Remote.multisearch("ahriman", pacman=pacman) == [aur_package_ahriman]
search_mock.assert_called_once_with("ahriman", pacman=pacman)
search_mock.assert_called_once_with("ahriman", pacman=pacman, search_by=None)
def test_remote_git_url(remote: Remote) -> None:
@ -69,8 +72,8 @@ def test_search(pacman: Pacman, mocker: MockerFixture) -> None:
must call search method
"""
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search")
Remote.search("ahriman", pacman=pacman)
search_mock.assert_called_once_with("ahriman", pacman=pacman)
Remote.search("ahriman", pacman=pacman, search_by="name")
search_mock.assert_called_once_with("ahriman", pacman=pacman, search_by="name")
def test_package_info(remote: Remote, pacman: Pacman) -> None:
@ -78,7 +81,7 @@ def test_package_info(remote: Remote, pacman: Pacman) -> None:
must raise NotImplemented for missing package info method
"""
with pytest.raises(NotImplementedError):
remote.package_info("package", pacman=pacman)
remote.package_info("package", pacman=pacman, include_provides=False)
def test_package_search(remote: Remote, pacman: Pacman) -> None:
@ -86,4 +89,4 @@ def test_package_search(remote: Remote, pacman: Pacman) -> None:
must raise NotImplemented for missing package search method
"""
with pytest.raises(NotImplementedError):
remote.package_search("package", pacman=pacman)
remote.package_search("package", pacman=pacman, search_by=None)

View File

@ -267,6 +267,14 @@ def test_package_empty(pacman: Pacman) -> None:
assert not list(pacman.package("some-random-name"))
def test_package_include_provides(pacman: Pacman) -> None:
"""
must return packages by provides list
"""
assert not list(pacman.package("sh", include_provides=False))
assert list(pacman.package("sh", include_provides=True))
def test_packages(pacman: Pacman) -> None:
"""
package list must not be empty