feat: extend result class

This commit is contained in:
Evgenii Alekseev 2023-11-10 17:09:01 +02:00
parent bb6414f9d4
commit fc8f6c2985
13 changed files with 155 additions and 87 deletions

View File

@ -167,12 +167,16 @@ class ApplicationPackages(ApplicationProperties):
""" """
raise NotImplementedError raise NotImplementedError
def remove(self, names: Iterable[str]) -> None: def remove(self, names: Iterable[str]) -> Result:
""" """
remove packages from repository remove packages from repository
Args: Args:
names(Iterable[str]): list of packages (either base or name) to remove names(Iterable[str]): list of packages (either base or name) to remove
Returns:
Result: removal result
""" """
self.repository.process_remove(names) result = self.repository.process_remove(names)
self.on_result(Result()) self.on_result(result)
return result

View File

@ -316,21 +316,6 @@ class UnknownPackageError(ValueError):
ValueError.__init__(self, f"Package base {package_base} is unknown") ValueError.__init__(self, f"Package base {package_base} is unknown")
class UnprocessedPackageStatusError(ValueError):
"""
exception for merging invalid statues
"""
def __init__(self, package_base: str) -> None:
"""
default constructor
Args:
package_base(str): package base name
"""
ValueError.__init__(self, f"Package base {package_base} had status failed, but new status is success")
class UnsafeRunError(RuntimeError): class UnsafeRunError(RuntimeError):
""" """
exception which will be raised in case if user is not owner of repository exception which will be raised in case if user is not owner of repository

View File

@ -120,6 +120,6 @@ class Email(Report, JinjaTemplate):
text = self.make_html(result, self.template) text = self.make_html(result, self.template)
attachments = {} attachments = {}
if self.template_full is not None: if self.template_full is not None:
attachments["index.html"] = self.make_html(Result(success=packages), self.template_full) attachments["index.html"] = self.make_html(Result(updated=packages), self.template_full)
self._send(text, attachments) self._send(text, attachments)

View File

@ -58,5 +58,5 @@ class HTML(Report, JinjaTemplate):
packages(list[Package]): list of packages to generate report packages(list[Package]): list of packages to generate report
result(Result): build result result(Result): build result
""" """
html = self.make_html(Result(success=packages), self.template) html = self.make_html(Result(updated=packages), self.template)
self.report_path.write_text(html, encoding="utf8") self.report_path.write_text(html, encoding="utf8")

View File

@ -98,7 +98,7 @@ class Executor(Cleaner):
try: try:
packager = self.packager(packagers, single.base) packager = self.packager(packagers, single.base)
build_single(single, Path(dir_name), packager.packager_id) build_single(single, Path(dir_name), packager.packager_id)
result.add_success(single) result.add_updated(single)
except Exception: except Exception:
self.reporter.set_failed(single.base) self.reporter.set_failed(single.base)
result.add_failed(single) result.add_failed(single)
@ -106,7 +106,7 @@ class Executor(Cleaner):
return result return result
def process_remove(self, packages: Iterable[str]) -> Path: def process_remove(self, packages: Iterable[str]) -> Result:
""" """
remove packages from list remove packages from list
@ -114,7 +114,7 @@ class Executor(Cleaner):
packages(Iterable[str]): list of package names or bases to remove packages(Iterable[str]): list of package names or bases to remove
Returns: Returns:
Path: path to repository database Result: remove result
""" """
def remove_base(package_base: str) -> None: def remove_base(package_base: str) -> None:
try: try:
@ -126,9 +126,9 @@ class Executor(Cleaner):
except Exception: except Exception:
self.logger.exception("could not remove base %s", package_base) self.logger.exception("could not remove base %s", package_base)
def remove_package(package: str, fn: Path) -> None: def remove_package(package: str, archive_path: Path) -> None:
try: try:
self.repo.remove(package, fn) # remove the package itself self.repo.remove(package, archive_path) # remove the package itself
except Exception: except Exception:
self.logger.exception("could not remove %s", package) self.logger.exception("could not remove %s", package)
@ -136,6 +136,7 @@ class Executor(Cleaner):
bases_to_remove: list[str] = [] bases_to_remove: list[str] = []
# build package list based on user input # build package list based on user input
result = Result()
requested = set(packages) requested = set(packages)
for local in self.packages(): for local in self.packages():
if local.base in packages or all(package in requested for package in local.packages): if local.base in packages or all(package in requested for package in local.packages):
@ -145,6 +146,7 @@ class Executor(Cleaner):
if properties.filepath is not None if properties.filepath is not None
}) })
bases_to_remove.append(local.base) bases_to_remove.append(local.base)
result.add_removed(local)
elif requested.intersection(local.packages.keys()): elif requested.intersection(local.packages.keys()):
packages_to_remove.update({ packages_to_remove.update({
package: properties.filepath package: properties.filepath
@ -167,7 +169,7 @@ class Executor(Cleaner):
for package in bases_to_remove: for package in bases_to_remove:
remove_base(package) remove_base(package)
return self.repo.repo_path return result
def process_update(self, packages: Iterable[Path], packagers: Packagers | None = None) -> Result: def process_update(self, packages: Iterable[Path], packagers: Packagers | None = None) -> Result:
""" """
@ -219,7 +221,7 @@ class Executor(Cleaner):
rename(description, local.base) rename(description, local.base)
update_single(description.filename, local.base, packager.key) update_single(description.filename, local.base, packager.key)
self.reporter.set_success(local) self.reporter.set_success(local)
result.add_success(local) result.add_updated(local)
current_package_archives: set[str] = set() current_package_archives: set[str] = set()
if local.base in current_packages: if local.base in current_packages:

View File

@ -19,28 +19,50 @@
# #
from __future__ import annotations from __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable, Callable
from typing import Any from typing import Any, Self
from ahriman.core.exceptions import UnprocessedPackageStatusError
from ahriman.models.package import Package from ahriman.models.package import Package
class Result: class Result:
""" """
build result class holder build result class holder
Attributes:
STATUS_PRIORITIES(list[str]): (class attribute) list of statues according to their priorities
""" """
def __init__(self, success: Iterable[Package] | None = None, failed: Iterable[Package] | None = None) -> None: STATUS_PRIORITIES = [
"failed",
"removed",
"updated",
"added",
]
def __init__(self, *, added: Iterable[Package] | None = None, updated: Iterable[Package] | None = None,
removed: Iterable[Package] | None = None, failed: Iterable[Package] | None = None) -> None:
""" """
default constructor default constructor
Args: Args:
success(Iterable[Package] | None, optional): initial list of successes packages (Default value = None) addded(Iterable[Package] | None, optional): initial list of successfully added packages
(Default value = None)
updated(Iterable[Package] | None, optional): initial list of successfully updated packages
(Default value = None)
removed(Iterable[Package] | None, optional): initial list of successfully removed packages
(Default value = None)
failed(Iterable[Package] | None, optional): initial list of failed packages (Default value = None) failed(Iterable[Package] | None, optional): initial list of failed packages (Default value = None)
""" """
success = success or [] added = added or []
self._success = {package.base: package for package in success} self._added = {package.base: package for package in added}
updated = updated or []
self._updated = {package.base: package for package in updated}
removed = removed or []
self._removed = {package.base: package for package in removed}
failed = failed or [] failed = failed or []
self._failed = {package.base: package for package in failed} self._failed = {package.base: package for package in failed}
@ -62,7 +84,17 @@ class Result:
Returns: Returns:
bool: True in case if success list is empty and False otherwise bool: True in case if success list is empty and False otherwise
""" """
return not bool(self._success) return not self._added and not self._updated
@property
def removed(self) -> list[Package]:
"""
get list of removed packages
Returns:
list[Package]: list of packages successfully removed
"""
return list(self._removed.values())
@property @property
def success(self) -> list[Package]: def success(self) -> list[Package]:
@ -72,7 +104,16 @@ class Result:
Returns: Returns:
list[Package]: list of packages with success result list[Package]: list of packages with success result
""" """
return list(self._success.values()) return list(self._added.values()) + list(self._updated.values())
def add_added(self, package: Package) -> None:
"""
add new package to new packages list
Args:
package(Package): package removed
"""
self._added[package.base] = package
def add_failed(self, package: Package) -> None: def add_failed(self, package: Package) -> None:
""" """
@ -83,17 +124,26 @@ class Result:
""" """
self._failed[package.base] = package self._failed[package.base] = package
def add_success(self, package: Package) -> None: def add_removed(self, package: Package) -> None:
"""
add new package to removed list
Args:
package(Package): package removed
"""
self._removed[package.base] = package
def add_updated(self, package: Package) -> None:
""" """
add new package to success built add new package to success built
Args: Args:
package(Package): package built package(Package): package built
""" """
self._success[package.base] = package self._updated[package.base] = package
# pylint: disable=protected-access # pylint: disable=protected-access
def merge(self, other: Result) -> Result: def merge(self, other: Result) -> Self:
""" """
merge other result into this one. This method assumes that other has fresh info about status and override it merge other result into this one. This method assumes that other has fresh info about status and override it
@ -101,19 +151,35 @@ class Result:
other(Result): instance of the newest result other(Result): instance of the newest result
Returns: Returns:
Result: updated instance Self: updated instance
Raises:
UnprocessedPackageStatusError: if there is previously failed package which is masked as success
""" """
for base, package in other._failed.items(): for status in self.STATUS_PRIORITIES:
if base in self._success: new_packages: Iterable[Package] = getattr(other, f"_{status}", {}).values()
del self._success[base] insert_package: Callable[[Package], None] = getattr(self, f"add_{status}")
self.add_failed(package) for package in new_packages:
for base, package in other._success.items(): insert_package(package)
if base in self._failed:
raise UnprocessedPackageStatusError(base) return self.refine()
self.add_success(package)
def refine(self) -> Self:
"""
merge packages between different results (e.g. remove failed from added, etc.) removing duplicates
Returns:
Self: updated instance
"""
for index, base_status in enumerate(self.STATUS_PRIORITIES):
# extract top-level packages
base_packages: Iterable[str] = getattr(self, f"_{base_status}", {}).keys()
# extract packages for each bottom-level
for status in self.STATUS_PRIORITIES[index + 1:]:
packages: dict[str, Package] = getattr(self, f"_{status}", {})
# if there is top-level package in bottom-level, then remove it
for base in base_packages:
if base in packages:
del packages[base]
return self return self
# required for tests at least # required for tests at least
@ -129,4 +195,7 @@ class Result:
""" """
if not isinstance(other, Result): if not isinstance(other, Result):
return False return False
return self.success == other.success and self.failed == other.failed return self._added == other._added \
and self._removed == other._removed \
and self._updated == other._updated \
and self._failed == other._failed

View File

@ -228,7 +228,7 @@ def test_remove(application_packages: ApplicationPackages, mocker: MockerFixture
""" """
must remove package must remove package
""" """
executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove") executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove", return_value=Result())
on_result_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages.on_result") on_result_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages.on_result")
application_packages.remove([]) application_packages.remove([])

View File

@ -77,7 +77,7 @@ def test_run_with_updates(args: argparse.Namespace, configuration: Configuration
args = _default_args(args) args = _default_args(args)
args.now = True args.now = True
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
mocker.patch("ahriman.application.application.Application.add") mocker.patch("ahriman.application.application.Application.add")
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result) application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result)

View File

@ -41,7 +41,7 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
""" """
args = _default_args(args) args = _default_args(args)
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
extract_mock = mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[package_ahriman]) extract_mock = mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[package_ahriman])
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on", application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on",

View File

@ -44,7 +44,7 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
""" """
args = _default_args(args) args = _default_args(args)
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result) application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result)
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")

View File

@ -526,7 +526,7 @@ def result(package_ahriman: Package) -> Result:
Result: result test instance Result: result test instance
""" """
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
return result return result

View File

@ -11,7 +11,7 @@ def test_generate(configuration: Configuration, package_ahriman: Package) -> Non
name = configuration.getpath("html", "template") name = configuration.getpath("html", "template")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
report = JinjaTemplate(repository_id, configuration, "html") report = JinjaTemplate(repository_id, configuration, "html")
assert report.make_html(Result(success=[package_ahriman]), name) assert report.make_html(Result(updated=[package_ahriman]), name)
def test_generate_from_path(configuration: Configuration, package_ahriman: Package) -> None: def test_generate_from_path(configuration: Configuration, package_ahriman: Package) -> None:
@ -21,4 +21,4 @@ def test_generate_from_path(configuration: Configuration, package_ahriman: Packa
path = configuration.getpath("html", "templates") / configuration.get("html", "template") path = configuration.getpath("html", "templates") / configuration.get("html", "template")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
report = JinjaTemplate(repository_id, configuration, "html") report = JinjaTemplate(repository_id, configuration, "html")
assert report.make_html(Result(success=[package_ahriman]), path) assert report.make_html(Result(updated=[package_ahriman]), path)

View File

@ -1,6 +1,3 @@
import pytest
from ahriman.core.exceptions import UnprocessedPackageStatusError
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.result import Result from ahriman.models.result import Result
@ -18,7 +15,7 @@ def test_non_empty_success(package_ahriman: Package) -> None:
must be non-empty if there is success build must be non-empty if there is success build
""" """
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
assert not result.is_empty assert not result.is_empty
@ -37,11 +34,22 @@ def test_non_empty_full(package_ahriman: Package) -> None:
""" """
result = Result() result = Result()
result.add_failed(package_ahriman) result.add_failed(package_ahriman)
result.add_success(package_ahriman) result.add_updated(package_ahriman)
assert not result.is_empty assert not result.is_empty
def test_add_added(package_ahriman: Package) -> None:
"""
must add package to new packages list
"""
result = Result()
result.add_added(package_ahriman)
assert not result.failed
assert not result.removed
assert result.success == [package_ahriman]
def test_add_failed(package_ahriman: Package) -> None: def test_add_failed(package_ahriman: Package) -> None:
""" """
must add package to failed list must add package to failed list
@ -49,17 +57,30 @@ def test_add_failed(package_ahriman: Package) -> None:
result = Result() result = Result()
result.add_failed(package_ahriman) result.add_failed(package_ahriman)
assert result.failed == [package_ahriman] assert result.failed == [package_ahriman]
assert not result.removed
assert not result.success assert not result.success
def test_add_success(package_ahriman: Package) -> None: def test_add_removed(package_ahriman: Package) -> None:
"""
must add package to removed list
"""
result = Result()
result.add_removed(package_ahriman)
assert not result.failed
assert result.removed == [package_ahriman]
assert not result.success
def test_add_updated(package_ahriman: Package) -> None:
""" """
must add package to success list must add package to success list
""" """
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
assert result.success == [package_ahriman]
assert not result.failed assert not result.failed
assert not result.removed
assert result.success == [package_ahriman]
def test_merge(package_ahriman: Package, package_python_schedule: Package) -> None: def test_merge(package_ahriman: Package, package_python_schedule: Package) -> None:
@ -67,9 +88,9 @@ def test_merge(package_ahriman: Package, package_python_schedule: Package) -> No
must merge success packages must merge success packages
""" """
left = Result() left = Result()
left.add_success(package_ahriman) left.add_updated(package_ahriman)
right = Result() right = Result()
right.add_success(package_python_schedule) right.add_updated(package_python_schedule)
result = left.merge(right) result = left.merge(right)
assert result.success == [package_ahriman, package_python_schedule] assert result.success == [package_ahriman, package_python_schedule]
@ -81,7 +102,7 @@ def test_merge_failed(package_ahriman: Package) -> None:
must merge and remove failed packages from success list must merge and remove failed packages from success list
""" """
left = Result() left = Result()
left.add_success(package_ahriman) left.add_updated(package_ahriman)
right = Result() right = Result()
right.add_failed(package_ahriman) right.add_failed(package_ahriman)
@ -90,28 +111,15 @@ def test_merge_failed(package_ahriman: Package) -> None:
assert not left.success assert not left.success
def test_merge_exception(package_ahriman: Package) -> None:
"""
must raise exception in case if package was failed
"""
left = Result()
left.add_failed(package_ahriman)
right = Result()
right.add_success(package_ahriman)
with pytest.raises(UnprocessedPackageStatusError):
left.merge(right)
def test_eq(package_ahriman: Package, package_python_schedule: Package) -> None: def test_eq(package_ahriman: Package, package_python_schedule: Package) -> None:
""" """
must return True for same objects must return True for same objects
""" """
left = Result() left = Result()
left.add_success(package_ahriman) left.add_updated(package_ahriman)
left.add_failed(package_python_schedule) left.add_failed(package_python_schedule)
right = Result() right = Result()
right.add_success(package_ahriman) right.add_updated(package_ahriman)
right.add_failed(package_python_schedule) right.add_failed(package_python_schedule)
assert left == right assert left == right
@ -122,7 +130,7 @@ def test_eq_false(package_ahriman: Package) -> None:
must return False in case if lists do not match must return False in case if lists do not match
""" """
left = Result() left = Result()
left.add_success(package_ahriman) left.add_updated(package_ahriman)
right = Result() right = Result()
right.add_failed(package_ahriman) right.add_failed(package_ahriman)
@ -144,7 +152,7 @@ def test_eq_false_success(package_ahriman: Package) -> None:
must return False in case if success does not match must return False in case if success does not match
""" """
left = Result() left = Result()
left.add_success(package_ahriman) left.add_updated(package_ahriman)
assert left != Result() assert left != Result()