From 44241b09d298cf1f086129a5d8bd2ae6cfe0579d Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Thu, 26 Jun 2025 11:05:15 +0300 Subject: [PATCH] support provides in aur --- .../application/application/application.py | 14 ++++---- src/ahriman/core/alpm/pacman.py | 14 ++++++-- src/ahriman/core/alpm/remote/aur.py | 23 ++++++++++--- src/ahriman/core/alpm/remote/official.py | 15 ++++++--- .../core/alpm/remote/official_syncdb.py | 10 ++++-- src/ahriman/core/alpm/remote/remote.py | 32 +++++++++++++------ src/ahriman/models/package.py | 12 ++++--- .../application/test_application.py | 16 ++++++---- tests/ahriman/core/alpm/remote/test_remote.py | 27 +++++++++------- tests/ahriman/core/alpm/test_pacman.py | 8 +++++ 10 files changed, 119 insertions(+), 52 deletions(-) diff --git a/src/ahriman/application/application/application.py b/src/ahriman/application/application/application.py index f6032a5c..2ae9874c 100644 --- a/src/ahriman/application/application/application.py +++ b/src/ahriman/application/application/application.py @@ -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 diff --git a/src/ahriman/core/alpm/pacman.py b/src/ahriman/core/alpm/pacman.py index 9f616699..376cc128 100644 --- a/src/ahriman/core/alpm/pacman.py +++ b/src/ahriman/core/alpm/pacman.py @@ -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 diff --git a/src/ahriman/core/alpm/remote/aur.py b/src/ahriman/core/alpm/remote/aur.py index d7df1a91..205b5ccb 100644 --- a/src/ahriman/core/alpm/remote/aur.py +++ b/src/ahriman/core/alpm/remote/aur.py @@ -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) diff --git a/src/ahriman/core/alpm/remote/official.py b/src/ahriman/core/alpm/remote/official.py index e931b1f9..6aaf9e8c 100644 --- a/src/ahriman/core/alpm/remote/official.py +++ b/src/ahriman/core/alpm/remote/official.py @@ -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) diff --git a/src/ahriman/core/alpm/remote/official_syncdb.py b/src/ahriman/core/alpm/remote/official_syncdb.py index 95abeb8b..1b0304c6 100644 --- a/src/ahriman/core/alpm/remote/official_syncdb.py +++ b/src/ahriman/core/alpm/remote/official_syncdb.py @@ -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 diff --git a/src/ahriman/core/alpm/remote/remote.py b/src/ahriman/core/alpm/remote/remote.py index 6e11b896..e2e45a71 100644 --- a/src/ahriman/core/alpm/remote/remote.py +++ b/src/ahriman/core/alpm/remote/remote.py @@ -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 diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index 27d7d6a3..4a74a893 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -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, diff --git a/tests/ahriman/application/application/test_application.py b/tests/ahriman/application/application/test_application.py index ec15b97f..5df929c4 100644 --- a/tests/ahriman/application/application/test_application.py +++ b/tests/ahriman/application/application/test_application.py @@ -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), diff --git a/tests/ahriman/core/alpm/remote/test_remote.py b/tests/ahriman/core/alpm/remote/test_remote.py index 8b5ddb38..45bcd1f4 100644 --- a/tests/ahriman/core/alpm/remote/test_remote.py +++ b/tests/ahriman/core/alpm/remote/test_remote.py @@ -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) diff --git a/tests/ahriman/core/alpm/test_pacman.py b/tests/ahriman/core/alpm/test_pacman.py index f3d0f48e..07c98d6c 100644 --- a/tests/ahriman/core/alpm/test_pacman.py +++ b/tests/ahriman/core/alpm/test_pacman.py @@ -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