diff --git a/src/ahriman/application/lock.py b/src/ahriman/application/lock.py index ea0dbe6d..1cede7f4 100644 --- a/src/ahriman/application/lock.py +++ b/src/ahriman/application/lock.py @@ -80,7 +80,7 @@ class Lock: :param exc_tb: exception traceback if any :return: always False (do not suppress any exception) """ - self.remove() + self.clear() status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed self.reporter.update_self(status) return False @@ -96,6 +96,14 @@ class Lock: if current_uid != root_uid: raise UnsafeRun(current_uid, root_uid) + def clear(self) -> None: + """ + remove lock file + """ + if self.path is None: + return + self.path.unlink(missing_ok=True) + def create(self) -> None: """ create lock file @@ -106,11 +114,3 @@ class Lock: self.path.touch(exist_ok=self.force) except FileExistsError: raise DuplicateRun() - - def remove(self) -> None: - """ - remove lock file - """ - if self.path is None: - return - self.path.unlink(missing_ok=True) diff --git a/src/ahriman/core/build_tools/task.py b/src/ahriman/core/build_tools/task.py index a64d74ff..93ad46b1 100644 --- a/src/ahriman/core/build_tools/task.py +++ b/src/ahriman/core/build_tools/task.py @@ -123,4 +123,4 @@ class Task: if self.cache_path.is_dir(): # no need to clone whole repository, just copy from cache first shutil.copytree(self.cache_path, git_path) - return Task.fetch(git_path, self.package.git_url) + return self.fetch(git_path, self.package.git_url) diff --git a/src/ahriman/core/report/report.py b/src/ahriman/core/report/report.py index 67b6b8b4..20406ae9 100644 --- a/src/ahriman/core/report/report.py +++ b/src/ahriman/core/report/report.py @@ -17,9 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from __future__ import annotations + import logging -from typing import Iterable +from typing import Iterable, Type from ahriman.core.configuration import Configuration from ahriman.core.exceptions import ReportFailed @@ -45,30 +47,34 @@ class Report: self.architecture = architecture self.configuration = configuration - @staticmethod - def run(architecture: str, configuration: Configuration, target: str, packages: Iterable[Package]) -> None: + @classmethod + def load(cls: Type[Report], architecture: str, configuration: Configuration, target: str) -> Report: """ - run report generation + load client from settings :param architecture: repository architecture :param configuration: configuration instance :param target: target to generate report (e.g. html) - :param packages: list of packages to generate report + :return: client according to current settings """ provider = ReportSettings.from_option(target) if provider == ReportSettings.HTML: from ahriman.core.report.html import HTML - report: Report = HTML(architecture, configuration) - else: - report = Report(architecture, configuration) - - try: - report.generate(packages) - except Exception: - report.logger.exception(f"report generation failed for target {provider.name}") - raise ReportFailed() + return HTML(architecture, configuration) + return cls(architecture, configuration) # should never happen def generate(self, packages: Iterable[Package]) -> None: """ generate report for the specified packages :param packages: list of packages to generate report """ + + def run(self, packages: Iterable[Package]) -> None: + """ + run report generation + :param packages: list of packages to generate report + """ + try: + self.generate(packages) + except Exception: + self.logger.exception("report generation failed") + raise ReportFailed() diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py index 5710ef55..2a94eb0f 100644 --- a/src/ahriman/core/repository/executor.py +++ b/src/ahriman/core/repository/executor.py @@ -108,7 +108,8 @@ class Executor(Cleaner): if targets is None: targets = self.configuration.getlist("report", "target") for target in targets: - Report.run(self.architecture, self.configuration, target, self.packages()) + runner = Report.load(self.architecture, self.configuration, target) + runner.run(self.packages()) def process_sync(self, targets: Optional[Iterable[str]]) -> None: """ @@ -118,7 +119,8 @@ class Executor(Cleaner): if targets is None: targets = self.configuration.getlist("upload", "target") for target in targets: - Upload.run(self.architecture, self.configuration, target, self.paths.repository) + runner = Upload.load(self.architecture, self.configuration, target) + runner.run(self.paths.repository) def process_update(self, packages: Iterable[Path]) -> Path: """ diff --git a/src/ahriman/core/status/client.py b/src/ahriman/core/status/client.py index 937d0a6f..d7bbfb79 100644 --- a/src/ahriman/core/status/client.py +++ b/src/ahriman/core/status/client.py @@ -19,7 +19,7 @@ # from __future__ import annotations -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Type from ahriman.core.configuration import Configuration from ahriman.models.build_status import BuildStatus, BuildStatusEnum @@ -31,6 +31,20 @@ class Client: base build status reporter client """ + @classmethod + def load(cls: Type[Client], configuration: Configuration) -> Client: + """ + load client from settings + :param configuration: configuration instance + :return: client according to current settings + """ + host = configuration.get("web", "host", fallback=None) + port = configuration.getint("web", "port", fallback=None) + if host is not None and port is not None: + from ahriman.core.status.web_client import WebClient + return WebClient(host, port) + return cls() + def add(self, package: Package, status: BuildStatusEnum) -> None: """ add new package with status @@ -109,18 +123,3 @@ class Client: :param package: current package properties """ return self.add(package, BuildStatusEnum.Unknown) - - @staticmethod - def load(configuration: Configuration) -> Client: - """ - load client from settings - :param configuration: configuration instance - :return: client according to current settings - """ - host = configuration.get("web", "host", fallback=None) - port = configuration.getint("web", "port", fallback=None) - if host is None or port is None: - return Client() - - from ahriman.core.status.web_client import WebClient - return WebClient(host, port) diff --git a/src/ahriman/core/upload/upload.py b/src/ahriman/core/upload/upload.py index 93de8626..c5016404 100644 --- a/src/ahriman/core/upload/upload.py +++ b/src/ahriman/core/upload/upload.py @@ -17,9 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from __future__ import annotations + import logging from pathlib import Path +from typing import Type from ahriman.core.configuration import Configuration from ahriman.core.exceptions import SyncFailed @@ -44,29 +47,33 @@ class Upload: self.architecture = architecture self.config = configuration - @staticmethod - def run(architecture: str, configuration: Configuration, target: str, path: Path) -> None: + @classmethod + def load(cls: Type[Upload], architecture: str, configuration: Configuration, target: str) -> Upload: """ - run remote sync + load client from settings :param architecture: repository architecture :param configuration: configuration instance :param target: target to run sync (e.g. s3) - :param path: local path to sync + :return: client according to current settings """ provider = UploadSettings.from_option(target) if provider == UploadSettings.Rsync: from ahriman.core.upload.rsync import Rsync - upload: Upload = Rsync(architecture, configuration) - elif provider == UploadSettings.S3: + return Rsync(architecture, configuration) + if provider == UploadSettings.S3: from ahriman.core.upload.s3 import S3 - upload = S3(architecture, configuration) - else: - upload = Upload(architecture, configuration) + return S3(architecture, configuration) + return cls(architecture, configuration) # should never happen + def run(self, path: Path) -> None: + """ + run remote sync + :param path: local path to sync + """ try: - upload.sync(path) + self.sync(path) except Exception: - upload.logger.exception(f"remote sync failed for {provider.name}") + self.logger.exception("remote sync failed") raise SyncFailed() def sync(self, path: Path) -> None: diff --git a/src/ahriman/models/build_status.py b/src/ahriman/models/build_status.py index 7489ff86..2dcbca1a 100644 --- a/src/ahriman/models/build_status.py +++ b/src/ahriman/models/build_status.py @@ -63,7 +63,7 @@ class BuildStatus: """ build status holder :ivar status: build status - :ivar _timestamp: build status update time + :ivar timestamp: build status update time """ def __init__(self, status: Union[BuildStatusEnum, str, None] = None, diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index d2a05fbd..17616284 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -156,6 +156,27 @@ class Package: aur_url=dump["aur_url"], packages=packages) + @classmethod + def load(cls: Type[Package], path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package: + """ + package constructor from available sources + :param path: one of path to sources directory, path to archive or package name/base + :param pacman: alpm wrapper instance (required to load from archive) + :param aur_url: AUR root url + :return: package properties + """ + try: + maybe_path = Path(path) + if maybe_path.is_dir(): + return cls.from_build(maybe_path, aur_url) + if maybe_path.is_file(): + return cls.from_archive(maybe_path, pacman, aur_url) + return cls.from_aur(str(path), aur_url) + except InvalidPackageInfo: + raise + except Exception as e: + raise InvalidPackageInfo(str(e)) + @staticmethod def dependencies(path: Path) -> Set[str]: """ @@ -194,29 +215,6 @@ class Package: prefix = f"{epoch}:" if epoch else "" return f"{prefix}{pkgver}-{pkgrel}" - @staticmethod - def load(path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package: - """ - package constructor from available sources - :param path: one of path to sources directory, path to archive or package name/base - :param pacman: alpm wrapper instance (required to load from archive) - :param aur_url: AUR root url - :return: package properties - """ - try: - maybe_path = Path(path) - if maybe_path.is_dir(): - package: Package = Package.from_build(maybe_path, aur_url) - elif maybe_path.is_file(): - package = Package.from_archive(maybe_path, pacman, aur_url) - else: - package = Package.from_aur(str(path), aur_url) - return package - except InvalidPackageInfo: - raise - except Exception as e: - raise InvalidPackageInfo(str(e)) - def actual_version(self, paths: RepositoryPaths) -> str: """ additional method to handle VCS package versions diff --git a/src/ahriman/models/package_description.py b/src/ahriman/models/package_description.py index e71a2e50..1a5f70d8 100644 --- a/src/ahriman/models/package_description.py +++ b/src/ahriman/models/package_description.py @@ -65,7 +65,7 @@ class PackageDescription: :param path: path to package archive :return: package properties based on tarball """ - return PackageDescription( + return cls( architecture=package.arch, archive_size=package.size, build_date=package.builddate, diff --git a/src/ahriman/models/report_settings.py b/src/ahriman/models/report_settings.py index f13bee33..d850e268 100644 --- a/src/ahriman/models/report_settings.py +++ b/src/ahriman/models/report_settings.py @@ -20,6 +20,7 @@ from __future__ import annotations from enum import Enum, auto +from typing import Type from ahriman.core.exceptions import InvalidOption @@ -32,13 +33,13 @@ class ReportSettings(Enum): HTML = auto() - @staticmethod - def from_option(value: str) -> ReportSettings: + @classmethod + def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings: """ construct value from configuration :param value: configuration value :return: parsed value """ if value.lower() in ("html",): - return ReportSettings.HTML + return cls.HTML raise InvalidOption(value) diff --git a/src/ahriman/models/sign_settings.py b/src/ahriman/models/sign_settings.py index 2919f720..918c38a2 100644 --- a/src/ahriman/models/sign_settings.py +++ b/src/ahriman/models/sign_settings.py @@ -20,6 +20,7 @@ from __future__ import annotations from enum import Enum, auto +from typing import Type from ahriman.core.exceptions import InvalidOption @@ -34,15 +35,15 @@ class SignSettings(Enum): SignPackages = auto() SignRepository = auto() - @staticmethod - def from_option(value: str) -> SignSettings: + @classmethod + def from_option(cls: Type[SignSettings], value: str) -> SignSettings: """ construct value from configuration :param value: configuration value :return: parsed value """ if value.lower() in ("package", "packages", "sign-package"): - return SignSettings.SignPackages + return cls.SignPackages if value.lower() in ("repository", "sign-repository"): - return SignSettings.SignRepository + return cls.SignRepository raise InvalidOption(value) diff --git a/src/ahriman/models/upload_settings.py b/src/ahriman/models/upload_settings.py index a892353a..1fec402c 100644 --- a/src/ahriman/models/upload_settings.py +++ b/src/ahriman/models/upload_settings.py @@ -20,6 +20,7 @@ from __future__ import annotations from enum import Enum, auto +from typing import Type from ahriman.core.exceptions import InvalidOption @@ -34,15 +35,15 @@ class UploadSettings(Enum): Rsync = auto() S3 = auto() - @staticmethod - def from_option(value: str) -> UploadSettings: + @classmethod + def from_option(cls: Type[UploadSettings], value: str) -> UploadSettings: """ construct value from configuration :param value: configuration value :return: parsed value """ if value.lower() in ("rsync",): - return UploadSettings.Rsync + return cls.Rsync if value.lower() in ("s3",): - return UploadSettings.S3 + return cls.S3 raise InvalidOption(value) diff --git a/tests/ahriman/application/test_lock.py b/tests/ahriman/application/test_lock.py index c3573558..58cf1956 100644 --- a/tests/ahriman/application/test_lock.py +++ b/tests/ahriman/application/test_lock.py @@ -15,14 +15,14 @@ def test_enter(lock: Lock, mocker: MockerFixture) -> None: must process with context manager """ check_user_mock = mocker.patch("ahriman.application.lock.Lock.check_user") - remove_mock = mocker.patch("ahriman.application.lock.Lock.remove") + clear_mock = mocker.patch("ahriman.application.lock.Lock.clear") create_mock = mocker.patch("ahriman.application.lock.Lock.create") update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self") with lock: pass check_user_mock.assert_called_once() - remove_mock.assert_called_once() + clear_mock.assert_called_once() create_mock.assert_called_once() update_status_mock.assert_has_calls([ mock.call(BuildStatusEnum.Building), @@ -35,7 +35,7 @@ def test_exit_with_exception(lock: Lock, mocker: MockerFixture) -> None: must process with context manager in case if exception raised """ mocker.patch("ahriman.application.lock.Lock.check_user") - mocker.patch("ahriman.application.lock.Lock.remove") + mocker.patch("ahriman.application.lock.Lock.clear") mocker.patch("ahriman.application.lock.Lock.create") update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self") @@ -79,6 +79,34 @@ def test_check_user_unsafe(lock: Lock) -> None: lock.check_user() +def test_clear(lock: Lock) -> None: + """ + must remove lock file + """ + lock.path = Path(tempfile.mktemp()) + lock.path.touch() + + lock.clear() + assert not lock.path.is_file() + + +def test_clear_missing(lock: Lock) -> None: + """ + must not fail on lock removal if file is missing + """ + lock.path = Path(tempfile.mktemp()) + lock.clear() + + +def test_clear_skip(lock: Lock, mocker: MockerFixture) -> None: + """ + must skip removal if no file set + """ + unlink_mock = mocker.patch("pathlib.Path.unlink") + lock.clear() + unlink_mock.assert_not_called() + + def test_create(lock: Lock) -> None: """ must create lock @@ -121,31 +149,3 @@ def test_create_unsafe(lock: Lock) -> None: lock.create() lock.path.unlink() - - -def test_remove(lock: Lock) -> None: - """ - must remove lock file - """ - lock.path = Path(tempfile.mktemp()) - lock.path.touch() - - lock.remove() - assert not lock.path.is_file() - - -def test_remove_missing(lock: Lock) -> None: - """ - must not fail on lock removal if file is missing - """ - lock.path = Path(tempfile.mktemp()) - lock.remove() - - -def test_remove_skip(lock: Lock, mocker: MockerFixture) -> None: - """ - must skip removal if no file set - """ - unlink_mock = mocker.patch("pathlib.Path.unlink") - lock.remove() - unlink_mock.assert_not_called() diff --git a/tests/ahriman/core/report/test_report.py b/tests/ahriman/core/report/test_report.py index c53bf649..2d1acbb8 100644 --- a/tests/ahriman/core/report/test_report.py +++ b/tests/ahriman/core/report/test_report.py @@ -15,7 +15,7 @@ def test_report_failure(configuration: Configuration, mocker: MockerFixture) -> """ mocker.patch("ahriman.core.report.html.HTML.generate", side_effect=Exception()) with pytest.raises(ReportFailed): - Report.run("x86_64", configuration, ReportSettings.HTML.name, Path("path")) + Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path")) def test_report_html(configuration: Configuration, mocker: MockerFixture) -> None: @@ -23,5 +23,5 @@ def test_report_html(configuration: Configuration, mocker: MockerFixture) -> Non must generate html report """ report_mock = mocker.patch("ahriman.core.report.html.HTML.generate") - Report.run("x86_64", configuration, ReportSettings.HTML.name, Path("path")) + Report.load("x86_64", configuration, ReportSettings.HTML.name).run(Path("path")) report_mock.assert_called_once() diff --git a/tests/ahriman/core/status/test_client.py b/tests/ahriman/core/status/test_client.py index 3a74b5c7..8b1c4e0a 100644 --- a/tests/ahriman/core/status/test_client.py +++ b/tests/ahriman/core/status/test_client.py @@ -7,6 +7,22 @@ from ahriman.models.build_status import BuildStatusEnum from ahriman.models.package import Package +def test_load_dummy_client(configuration: Configuration) -> None: + """ + must load dummy client if no settings set + """ + assert isinstance(Client.load(configuration), Client) + + +def test_load_full_client(configuration: Configuration) -> None: + """ + must load full client if no settings set + """ + configuration.set("web", "host", "localhost") + configuration.set("web", "port", "8080") + assert isinstance(Client.load(configuration), WebClient) + + def test_add(client: Client, package_ahriman: Package) -> None: """ must process package addition without errors @@ -98,19 +114,3 @@ def test_set_unknown(client: Client, package_ahriman: Package, mocker: MockerFix client.set_unknown(package_ahriman) add_mock.assert_called_with(package_ahriman, BuildStatusEnum.Unknown) - - -def test_load_dummy_client(configuration: Configuration) -> None: - """ - must load dummy client if no settings set - """ - assert isinstance(Client.load(configuration), Client) - - -def test_load_full_client(configuration: Configuration) -> None: - """ - must load full client if no settings set - """ - configuration.set("web", "host", "localhost") - configuration.set("web", "port", "8080") - assert isinstance(Client.load(configuration), WebClient) diff --git a/tests/ahriman/core/test_configuration.py b/tests/ahriman/core/test_configuration.py index 36fadbe7..198dae0d 100644 --- a/tests/ahriman/core/test_configuration.py +++ b/tests/ahriman/core/test_configuration.py @@ -14,8 +14,8 @@ def test_from_path(mocker: MockerFixture) -> None: load_logging_mock = mocker.patch("ahriman.core.configuration.Configuration.load_logging") path = Path("path") - config = Configuration.from_path(path, "x86_64", True) - assert config.path == path + configuration = Configuration.from_path(path, "x86_64", True) + assert configuration.path == path read_mock.assert_called_with(path) load_includes_mock.assert_called_once() load_logging_mock.assert_called_once() diff --git a/tests/ahriman/core/upload/test_uploader.py b/tests/ahriman/core/upload/test_upload.py similarity index 79% rename from tests/ahriman/core/upload/test_uploader.py rename to tests/ahriman/core/upload/test_upload.py index 1e1c5140..99815c57 100644 --- a/tests/ahriman/core/upload/test_uploader.py +++ b/tests/ahriman/core/upload/test_upload.py @@ -15,7 +15,7 @@ def test_upload_failure(configuration: Configuration, mocker: MockerFixture) -> """ mocker.patch("ahriman.core.upload.rsync.Rsync.sync", side_effect=Exception()) with pytest.raises(SyncFailed): - Upload.run("x86_64", configuration, UploadSettings.Rsync.name, Path("path")) + Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path")) def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> None: @@ -23,7 +23,7 @@ def test_upload_rsync(configuration: Configuration, mocker: MockerFixture) -> No must upload via rsync """ upload_mock = mocker.patch("ahriman.core.upload.rsync.Rsync.sync") - Upload.run("x86_64", configuration, UploadSettings.Rsync.name, Path("path")) + Upload.load("x86_64", configuration, UploadSettings.Rsync.name).run(Path("path")) upload_mock.assert_called_once() @@ -32,5 +32,5 @@ def test_upload_s3(configuration: Configuration, mocker: MockerFixture) -> None: must upload via s3 """ upload_mock = mocker.patch("ahriman.core.upload.s3.S3.sync") - Upload.run("x86_64", configuration, UploadSettings.S3.name, Path("path")) + Upload.load("x86_64", configuration, UploadSettings.S3.name).run(Path("path")) upload_mock.assert_called_once() diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py index e6847232..a547a8bf 100644 --- a/tests/ahriman/models/test_package.py +++ b/tests/ahriman/models/test_package.py @@ -135,24 +135,6 @@ def test_from_json_view_3(package_tpacpi_bat_git: Package) -> None: assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git -def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Path) -> None: - """ - must load correct list of dependencies with version - """ - srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text() - mocker.patch("pathlib.Path.read_text", return_value=srcinfo) - - assert Package.dependencies(Path("path")) == {"git", "go", "pacman"} - - -def test_full_version() -> None: - """ - must construct full version - """ - assert Package.full_version("1", "r2388.d30e3201", "1") == "1:r2388.d30e3201-1" - assert Package.full_version(None, "0.12.1", "1") == "0.12.1-1" - - def test_load_from_archive(package_ahriman: Package, pyalpm_handle: MagicMock, mocker: MockerFixture) -> None: """ must load package from package archive @@ -198,6 +180,24 @@ def test_load_failure(package_ahriman: Package, pyalpm_handle: MagicMock, mocker Package.load(Path("path"), pyalpm_handle, package_ahriman.aur_url) +def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Path) -> None: + """ + must load correct list of dependencies with version + """ + srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text() + mocker.patch("pathlib.Path.read_text", return_value=srcinfo) + + assert Package.dependencies(Path("path")) == {"git", "go", "pacman"} + + +def test_full_version() -> None: + """ + must construct full version + """ + assert Package.full_version("1", "r2388.d30e3201", "1") == "1:r2388.d30e3201-1" + assert Package.full_version(None, "0.12.1", "1") == "0.12.1-1" + + def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None: """ must return same actual_version as version is