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

View File

@ -223,22 +223,32 @@ class Pacman(LazyLogging):
return result 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: Args:
package_name(str): package name to search package_name(str): package name to search
include_provides(bool, optional): search by provides if no exact match found (Default value = False)
Yields: Yields:
Package: list of packages which were returned by the query 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(): for database in self.handle.get_syncdbs():
package = database.get_pkg(package_name) package = database.get_pkg(package_name)
if package is None: if package is None:
continue continue
yield package 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]: def packages(self) -> set[str]:
""" """
get list of packages known for alpm 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) response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
return self.parse_response(response.json()) 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: Args:
package_name(str): package name to search package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search pacman(Pacman | None): alpm wrapper instance, required for official repositories search
include_provides(bool): search by provides if no exact match found
Returns: Returns:
AURPackage: package which match the package name AURPackage: package which match the package name
@ -127,21 +131,32 @@ class AUR(Remote):
Raises: Raises:
UnknownPackageError: package doesn't exist 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) packages = self.aur_request("info", package_name)
try: try:
return next(package for package in packages if package.name == package_name) return next(package for package in packages if package.name == package_name)
except StopIteration: 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 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 search package in AUR web
Args: Args:
*keywords(str): keywords to search *keywords(str): keywords to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search pacman(Pacman | None): alpm wrapper instance, required for official repositories search
search_by(str | None): search by keywords
Returns: Returns:
list[AURPackage]: list of packages which match the criteria 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) response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
return self.parse_response(response.json()) 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: Args:
package_name(str): package name to search package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search pacman(Pacman | None): alpm wrapper instance, required for official repositories search
include_provides(bool): search by provides if no exact match found
Returns: Returns:
AURPackage: package which match the package name AURPackage: package which match the package name
@ -125,17 +129,20 @@ class Official(Remote):
try: try:
return next(package for package in packages if package.name == package_name) return next(package for package in packages if package.name == package_name)
except StopIteration: except StopIteration:
# it does not support search by provides
raise UnknownPackageError(package_name) from None 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 search package in AUR web
Args: Args:
*keywords(str): keywords to search *keywords(str): keywords to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search pacman(Pacman | None): alpm wrapper instance, required for official repositories search
search_by(str | None): search by keywords
Returns: Returns:
list[AURPackage]: list of packages which match the criteria 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. 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: Args:
package_name(str): package name to search package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search pacman(Pacman | None): alpm wrapper instance, required for official repositories search
include_provides(bool): search by provides if no exact match found
Returns: Returns:
AURPackage: package which match the package name AURPackage: package which match the package name
@ -56,6 +60,6 @@ class OfficialSyncdb(Official):
raise UnknownPackageError(package_name) raise UnknownPackageError(package_name)
try: 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: except StopIteration:
raise UnknownPackageError(package_name) from None raise UnknownPackageError(package_name) from None

View File

@ -41,22 +41,27 @@ class Remote(SyncHttpClient):
""" """
@classmethod @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: Args:
package_name(str): package name to search package_name(str): package name to search
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None) (Default value = None)
include_provides(bool, optional): search by provides if no exact match found (Default value = False)
Returns: Returns:
AURPackage: package which match the package name 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 @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 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 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" *keywords(str): search terms, e.g. "ahriman", "is", "cool"
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None) (Default value = None)
search_by(str | None, optional): search by keywords (Default value = None)
Returns: Returns:
list[AURPackage]: list of packages each of them matches all search terms list[AURPackage]: list of packages each of them matches all search terms
@ -72,7 +78,7 @@ class Remote(SyncHttpClient):
instance = cls() instance = cls()
packages: dict[str, AURPackage] = {} packages: dict[str, AURPackage] = {}
for term in filter(lambda word: len(word) >= 3, keywords): 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 = { packages = {
package.name: package # not mistake to group them by name package.name: package # not mistake to group them by name
for package in portion for package in portion
@ -114,7 +120,7 @@ class Remote(SyncHttpClient):
raise NotImplementedError raise NotImplementedError
@classmethod @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 search package in AUR web
@ -122,19 +128,24 @@ class Remote(SyncHttpClient):
*keywords(str): search terms, e.g. "ahriman", "is", "cool" *keywords(str): search terms, e.g. "ahriman", "is", "cool"
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None) (Default value = None)
search_by(str | None, optional): search by keywords (Default value = None)
Returns: Returns:
list[AURPackage]: list of packages which match the criteria 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: Args:
package_name(str): package name to search package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search pacman(Pacman | None): alpm wrapper instance, required for official repositories search
include_provides(bool): search by provides if no exact match found
Returns: Returns:
AURPackage: package which match the package name AURPackage: package which match the package name
@ -144,13 +155,14 @@ class Remote(SyncHttpClient):
""" """
raise NotImplementedError 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 search package in AUR web
Args: Args:
*keywords(str): keywords to search *keywords(str): keywords to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search pacman(Pacman | None): alpm wrapper instance, required for official repositories search
search_by(str | None): search by keywords
Returns: Returns:
list[AURPackage]: list of packages which match the criteria list[AURPackage]: list of packages which match the criteria

View File

@ -213,18 +213,19 @@ class Package(LazyLogging):
) )
@classmethod @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 construct package properties from AUR page
Args: Args:
name(str): package name (either base or normal name) name(str): package name (either base or normal name)
packager(str | None, optional): packager to be used for this build (Default value = None) 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: Returns:
Self: package properties Self: package properties
""" """
package = AUR.info(name) package = AUR.info(name, include_provides=include_provides)
remote = RemoteSource( remote = RemoteSource(
source=PackageSource.AUR, source=PackageSource.AUR,
@ -310,7 +311,8 @@ class Package(LazyLogging):
) )
@classmethod @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 construct package properties from official repository page
@ -319,11 +321,13 @@ class Package(LazyLogging):
pacman(Pacman): alpm wrapper instance pacman(Pacman): alpm wrapper instance
packager(str | None, optional): packager to be used for this build (Default value = None) 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) 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: Returns:
Self: package properties 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( remote = RemoteSource(
source=PackageSource.Repository, source=PackageSource.Repository,

View File

@ -1,4 +1,6 @@
from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import MagicMock, call as MockCall from unittest.mock import MagicMock, call as MockCall
from ahriman.application.application import Application 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] mock.packages_full = [package_base]
return mock 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.packages = {
package_python_schedule.base: package_python_schedule.packages[package_python_schedule.base] 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") 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", package_aur_mock = mocker.patch("ahriman.models.package.Package.from_aur", side_effect=get_package)
side_effect=lambda *args: packages[args[0]]) package_local_mock = mocker.patch("ahriman.models.package.Package.from_build", side_effect=get_package)
package_local_mock = mocker.patch("ahriman.models.package.Package.from_build",
side_effect=lambda *args: packages[args[0].name])
packages_mock = mocker.patch("ahriman.application.application.Application._known_packages", packages_mock = mocker.patch("ahriman.application.application.Application._known_packages",
return_value={"devtools", "python-build", "python-pytest"}) return_value={"devtools", "python-build", "python-pytest"})
status_client_mock = mocker.patch("ahriman.core.status.Client.set_unknown") 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) result = application.with_dependencies([package_ahriman], process_dependencies=True)
assert {package.base: package for package in result} == packages assert {package.base: package for package in result} == packages
package_aur_mock.assert_has_calls([ package_aur_mock.assert_has_calls([
MockCall(package_python_schedule.base, package_ahriman.packager), MockCall(package_python_schedule.base, package_ahriman.packager, include_provides=True),
MockCall("python-installer", package_ahriman.packager), MockCall("python-installer", package_ahriman.packager, include_provides=True),
], any_order=True) ], any_order=True)
package_local_mock.assert_has_calls([ package_local_mock.assert_has_calls([
MockCall(application.repository.paths.cache_for("python"), "x86_64", package_ahriman.packager), 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 must call info method
""" """
info_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_info") info_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_info")
Remote.info("ahriman", pacman=pacman) Remote.info("ahriman", pacman=pacman, include_provides=True)
info_mock.assert_called_once_with("ahriman", pacman=pacman) info_mock.assert_called_once_with("ahriman", pacman=pacman, include_provides=True)
def test_multisearch(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None: 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 must search in AUR with multiple words
""" """
terms = ["ahriman", "is", "cool"] 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] assert Remote.multisearch(*terms, pacman=pacman, search_by="name") == [aur_package_ahriman]
search_mock.assert_has_calls([MockCall("ahriman", pacman=pacman), MockCall("cool", pacman=pacman)]) 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: 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 must return empty list if no long terms supplied
""" """
terms = ["it", "is"] 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) == [] assert Remote.multisearch(*terms, pacman=pacman) == []
search_mock.assert_not_called() 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 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] 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: def test_remote_git_url(remote: Remote) -> None:
@ -69,8 +72,8 @@ def test_search(pacman: Pacman, mocker: MockerFixture) -> None:
must call search method must call search method
""" """
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search") search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search")
Remote.search("ahriman", pacman=pacman) Remote.search("ahriman", pacman=pacman, search_by="name")
search_mock.assert_called_once_with("ahriman", pacman=pacman) search_mock.assert_called_once_with("ahriman", pacman=pacman, search_by="name")
def test_package_info(remote: Remote, pacman: Pacman) -> None: 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 must raise NotImplemented for missing package info method
""" """
with pytest.raises(NotImplementedError): 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: 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 must raise NotImplemented for missing package search method
""" """
with pytest.raises(NotImplementedError): 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")) 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: def test_packages(pacman: Pacman) -> None:
""" """
package list must not be empty package list must not be empty