diff --git a/docs/architecture.md b/docs/architecture.md
index 5c7b7d8e..8839272f 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -16,6 +16,8 @@ This package contains application (aka executable) related classes and everythin
`ahriman.application.application.application.Application` (god class) is used for any interaction from parsers with repository, web etc. It is divided into multiple traits by functions (package related and repository related) in the same package.
+`ahriman.application.formatters` package provides `Printer` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
+
`ahriman.application.ahriman` contains only command line parses and executes specified `Handler` on success, `ahriman.application.lock.Lock` is additional class which provides file-based lock and also performs some common checks.
## `ahriman.core` package
diff --git a/docs/faq.md b/docs/faq.md
index 704a9e37..ddd057ab 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -399,6 +399,17 @@ Don't know, haven't tried it. But it lacks of documentation at least.
* `archrepo2` actively uses direct shell calls and `yaourt` components.
* It has constantly running process instead of timer process (it is not pro or con).
+#### [repoctl](https://github.com/cassava/repoctl)
+
+* Web interface.
+* No reporting.
+* Local packages and patches support.
+* Some actions are not fully automated (e.g. package update still requires manual intervention for the build itself).
+* `repoctl` has better AUR interaction features. With colors!
+* `repoctl` has much easier configuration and even completion.
+* `repoctl` is able to store old packages.
+* Ability to host repository from same command vs external services (e.g. nginx) in `ahriman`.
+
#### [repo-scripts](https://github.com/arcan1s/repo-scripts)
Though originally I've created ahriman by trying to improve the project, it still lacks a lot of features:
diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py
index 069d2a53..53d7f127 100644
--- a/src/ahriman/application/ahriman.py
+++ b/src/ahriman/application/ahriman.py
@@ -60,7 +60,8 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("-l", "--lock", help="lock file", type=Path,
default=Path(tempfile.gettempdir()) / "ahriman.lock")
parser.add_argument("--no-report", help="force disable reporting to web service", action="store_true")
- parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")
+ parser.add_argument("-q", "--quiet", help="force disable any logging", action=argparse.BooleanOptionalAction,
+ default=False) # sometimes we would like to run not quiet even if it is disabled by default
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable",
action="store_true")
parser.add_argument("-v", "--version", action="version", version=version.__version__)
@@ -104,7 +105,12 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
parser = root.add_parser("aur-search", aliases=["search"], help="search for package",
description="search for package in AUR using API", formatter_class=_formatter)
- parser.add_argument("search", help="search terms, can be specified multiple times", nargs="+")
+ parser.add_argument("search", help="search terms, can be specified multiple times, result will match all terms",
+ nargs="+")
+ parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
+ parser.add_argument("--sort-by", help="sort field by this field. In case if two packages have the same value of "
+ "the specified field, they will be always sorted by name",
+ default="name", choices=sorted(handlers.Search.SORT_FIELDS))
parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, no_report=True, quiet=True, unsafe=True)
return parser
@@ -181,6 +187,7 @@ def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser
formatter_class=_formatter)
parser.add_argument("package", help="filter status by package base", nargs="*")
parser.add_argument("--ahriman", help="get service status itself", action="store_true")
+ parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status",
type=BuildStatusEnum, choices=BuildStatusEnum)
parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, quiet=True, unsafe=True)
@@ -354,6 +361,7 @@ def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentP
description="remove packages which are missing in AUR and do not have local PKGBUILDs",
formatter_class=_formatter)
parser.add_argument("--dry-run", help="just perform check for packages without removal", action="store_true")
+ parser.add_argument("-i", "--info", help="show additional package information", action="store_true")
parser.set_defaults(handler=handlers.RemoveUnknown)
return parser
diff --git a/src/ahriman/application/formatters/__init__.py b/src/ahriman/application/formatters/__init__.py
new file mode 100644
index 00000000..fb32931e
--- /dev/null
+++ b/src/ahriman/application/formatters/__init__.py
@@ -0,0 +1,19 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
diff --git a/src/ahriman/application/formatters/aur_printer.py b/src/ahriman/application/formatters/aur_printer.py
new file mode 100644
index 00000000..dddc077c
--- /dev/null
+++ b/src/ahriman/application/formatters/aur_printer.py
@@ -0,0 +1,62 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+import aur # type: ignore
+
+from typing import List, Optional
+
+from ahriman.application.formatters.printer import Printer
+from ahriman.core.util import pretty_datetime
+from ahriman.models.property import Property
+
+
+class AurPrinter(Printer):
+ """
+ print content of the AUR package
+ """
+
+ def __init__(self, package: aur.Package) -> None:
+ """
+ default constructor
+ :param package: AUR package description
+ """
+ self.content = package
+
+ def properties(self) -> List[Property]:
+ """
+ convert content into printable data
+ :return: list of content properties
+ """
+ return [
+ Property("Package base", self.content.package_base),
+ Property("Description", self.content.description, is_required=True),
+ Property("Upstream URL", self.content.url),
+ Property("Licenses", self.content.license), # it should be actually a list
+ Property("Maintainer", self.content.maintainer or ""), # I think it is optional
+ Property("First submitted", pretty_datetime(self.content.first_submitted)),
+ Property("Last updated", pretty_datetime(self.content.last_modified)),
+ # more fields coming https://github.com/cdown/aur/pull/29
+ ]
+
+ def title(self) -> Optional[str]:
+ """
+ generate entry title from content
+ :return: content title if it can be generated and None otherwise
+ """
+ return f"{self.content.name} {self.content.version} ({self.content.num_votes})"
diff --git a/src/ahriman/application/formatters/configuration_printer.py b/src/ahriman/application/formatters/configuration_printer.py
new file mode 100644
index 00000000..6a725747
--- /dev/null
+++ b/src/ahriman/application/formatters/configuration_printer.py
@@ -0,0 +1,55 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from typing import Dict, List, Optional
+
+from ahriman.application.formatters.printer import Printer
+from ahriman.models.property import Property
+
+
+class ConfigurationPrinter(Printer):
+ """
+ print content of the configuration section
+ """
+
+ def __init__(self, section: str, values: Dict[str, str]) -> None:
+ """
+ default constructor
+ :param section: section name
+ :param values: configuration values dictionary
+ """
+ self.section = section
+ self.content = values
+
+ def properties(self) -> List[Property]:
+ """
+ convert content into printable data
+ :return: list of content properties
+ """
+ return [
+ Property(key, value, is_required=True)
+ for key, value in sorted(self.content.items())
+ ]
+
+ def title(self) -> Optional[str]:
+ """
+ generate entry title from content
+ :return: content title if it can be generated and None otherwise
+ """
+ return f"[{self.section}]"
diff --git a/src/ahriman/application/formatters/package_printer.py b/src/ahriman/application/formatters/package_printer.py
new file mode 100644
index 00000000..d5fe80a0
--- /dev/null
+++ b/src/ahriman/application/formatters/package_printer.py
@@ -0,0 +1,60 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from typing import List, Optional
+
+from ahriman.application.formatters.printer import Printer
+from ahriman.models.build_status import BuildStatus
+from ahriman.models.package import Package
+from ahriman.models.property import Property
+
+
+class PackagePrinter(Printer):
+ """
+ print content of the internal package object
+ """
+
+ def __init__(self, package: Package, status: BuildStatus) -> None:
+ """
+ default constructor
+ :param package: package description
+ :param status: build status
+ """
+ self.content = package
+ self.status = status
+
+ def properties(self) -> List[Property]:
+ """
+ convert content into printable data
+ :return: list of content properties
+ """
+ return [
+ Property("Version", self.content.version, is_required=True),
+ Property("Groups", " ".join(self.content.groups)),
+ Property("Licenses", " ".join(self.content.licenses)),
+ Property("Depends", " ".join(self.content.depends)),
+ Property("Status", self.status.pretty_print(), is_required=True),
+ ]
+
+ def title(self) -> Optional[str]:
+ """
+ generate entry title from content
+ :return: content title if it can be generated and None otherwise
+ """
+ return self.content.pretty_print()
diff --git a/src/ahriman/application/formatters/printer.py b/src/ahriman/application/formatters/printer.py
new file mode 100644
index 00000000..c0404dfe
--- /dev/null
+++ b/src/ahriman/application/formatters/printer.py
@@ -0,0 +1,55 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from typing import Callable, List, Optional
+
+from ahriman.models.property import Property
+
+
+class Printer:
+ """
+ base class for formatters
+ """
+
+ def print(self, verbose: bool, log_fn: Callable[[str], None] = print, separator: str = ": ") -> None:
+ """
+ print content
+ :param verbose: print all fields
+ :param log_fn: logger function to log data
+ :param separator: separator for property name and property value
+ """
+ if (title := self.title()) is not None:
+ log_fn(title)
+ for prop in self.properties():
+ if not verbose and not prop.is_required:
+ continue
+ log_fn(f"\t{prop.name}{separator}{prop.value}")
+
+ def properties(self) -> List[Property]: # pylint: disable=no-self-use
+ """
+ convert content into printable data
+ :return: list of content properties
+ """
+ return []
+
+ def title(self) -> Optional[str]:
+ """
+ generate entry title from content
+ :return: content title if it can be generated and None otherwise
+ """
diff --git a/src/ahriman/application/formatters/status_printer.py b/src/ahriman/application/formatters/status_printer.py
new file mode 100644
index 00000000..9293b0ea
--- /dev/null
+++ b/src/ahriman/application/formatters/status_printer.py
@@ -0,0 +1,51 @@
+#
+# Copyright (c) 2021 ahriman team.
+#
+# This file is part of ahriman
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from typing import List, Optional
+
+from ahriman.application.formatters.printer import Printer
+from ahriman.models.build_status import BuildStatus
+from ahriman.models.property import Property
+
+
+class StatusPrinter(Printer):
+ """
+ print content of the status object
+ """
+
+ def __init__(self, status: BuildStatus) -> None:
+ """
+ default constructor
+ :param status: build status
+ """
+ self.content = status
+
+ def properties(self) -> List[Property]:
+ """
+ convert content into printable data
+ :return: list of content properties
+ """
+ return []
+
+ def title(self) -> Optional[str]:
+ """
+ generate entry title from content
+ :return: content title if it can be generated and None otherwise
+ """
+ return self.content.pretty_print()
diff --git a/src/ahriman/application/handlers/dump.py b/src/ahriman/application/handlers/dump.py
index 0e920faf..42ae504e 100644
--- a/src/ahriman/application/handlers/dump.py
+++ b/src/ahriman/application/handlers/dump.py
@@ -21,6 +21,7 @@ import argparse
from typing import Type
+from ahriman.application.formatters.configuration_printer import ConfigurationPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
@@ -32,8 +33,6 @@ class Dump(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False
- _print = print
-
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
configuration: Configuration, no_report: bool) -> None:
@@ -46,7 +45,4 @@ class Dump(Handler):
"""
dump = configuration.dump()
for section, values in sorted(dump.items()):
- Dump._print(f"[{section}]")
- for key, value in sorted(values.items()):
- Dump._print(f"{key} = {value}")
- Dump._print()
+ ConfigurationPrinter(section, values).print(verbose=False, separator=" = ")
diff --git a/src/ahriman/application/handlers/remove_unknown.py b/src/ahriman/application/handlers/remove_unknown.py
index 39cc2a60..b7ece2d9 100644
--- a/src/ahriman/application/handlers/remove_unknown.py
+++ b/src/ahriman/application/handlers/remove_unknown.py
@@ -22,9 +22,10 @@ import argparse
from typing import Type
from ahriman.application.application import Application
+from ahriman.application.formatters.package_printer import PackagePrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
-from ahriman.models.package import Package
+from ahriman.models.build_status import BuildStatus
class RemoveUnknown(Handler):
@@ -46,16 +47,7 @@ class RemoveUnknown(Handler):
unknown_packages = application.unknown()
if args.dry_run:
for package in unknown_packages:
- RemoveUnknown.log_fn(package)
+ PackagePrinter(package, BuildStatus()).print(args.info)
return
application.remove(package.base for package in unknown_packages)
-
- @staticmethod
- def log_fn(package: Package) -> None:
- """
- log package information
- :param package: package object to log
- """
- print(f"=> {package.base} {package.version}")
- print(f" {package.web_url}")
diff --git a/src/ahriman/application/handlers/search.py b/src/ahriman/application/handlers/search.py
index ce47dbe5..a999dc53 100644
--- a/src/ahriman/application/handlers/search.py
+++ b/src/ahriman/application/handlers/search.py
@@ -20,10 +20,12 @@
import argparse
import aur # type: ignore
-from typing import Callable, Type
+from typing import Callable, Dict, Iterable, List, Tuple, Type
+from ahriman.application.formatters.aur_printer import AurPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
+from ahriman.core.exceptions import InvalidOption
class Search(Handler):
@@ -32,6 +34,7 @@ class Search(Handler):
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
+ SORT_FIELDS = set(aur.Package._fields) # later we will have to remove some fields from here (lists)
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str,
@@ -43,20 +46,32 @@ class Search(Handler):
:param configuration: configuration instance
:param no_report: force disable reporting
"""
- search = " ".join(args.search)
- packages = aur.search(search)
+ packages: Dict[str, aur.Package] = {}
+ # see https://bugs.archlinux.org/task/49133
+ for search in args.search:
+ portion = aur.search(search)
+ packages = {
+ package.package_base: package
+ for package in portion
+ if package.package_base in packages or not packages
+ }
- # it actually always should return string
- # explicit cast to string just to avoid mypy warning for untyped library
- comparator: Callable[[aur.Package], str] = lambda item: str(item.package_base)
- for package in sorted(packages, key=comparator):
- Search.log_fn(package)
+ packages_list = list(packages.values()) # explicit conversion for the tests
+ for package in Search.sort(packages_list, args.sort_by):
+ AurPrinter(package).print(args.info)
@staticmethod
- def log_fn(package: aur.Package) -> None:
+ def sort(packages: Iterable[aur.Package], sort_by: str) -> List[aur.Package]:
"""
- log package information
- :param package: package object as from AUR
+ sort package list by specified field
+ :param packages: packages list to sort
+ :param sort_by: AUR package field name to sort by
+ :return: sorted list for packages
"""
- print(f"=> {package.package_base} {package.version}")
- print(f" {package.description}")
+ if sort_by not in Search.SORT_FIELDS:
+ raise InvalidOption(sort_by)
+ # always sort by package name at the last
+ # well technically it is not a string, but we can deal with it
+ comparator: Callable[[aur.Package], Tuple[str, str]] =\
+ lambda package: (getattr(package, sort_by), package.name)
+ return sorted(packages, key=comparator)
diff --git a/src/ahriman/application/handlers/status.py b/src/ahriman/application/handlers/status.py
index 47d1b745..31b8c435 100644
--- a/src/ahriman/application/handlers/status.py
+++ b/src/ahriman/application/handlers/status.py
@@ -22,6 +22,8 @@ import argparse
from typing import Callable, Iterable, Tuple, Type
from ahriman.application.application import Application
+from ahriman.application.formatters.package_printer import PackagePrinter
+from ahriman.application.formatters.status_printer import StatusPrinter
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus
@@ -49,8 +51,7 @@ class Status(Handler):
client = Application(architecture, configuration, no_report=False).repository.reporter
if args.ahriman:
ahriman = client.get_self()
- print(ahriman.pretty_print())
- print()
+ StatusPrinter(ahriman).print(args.info)
if args.package:
packages: Iterable[Tuple[Package, BuildStatus]] = sum(
[client.get(base) for base in args.package],
@@ -62,6 +63,4 @@ class Status(Handler):
filter_fn: Callable[[Tuple[Package, BuildStatus]], bool] =\
lambda item: args.status is None or item[1].status == args.status
for package, package_status in sorted(filter(filter_fn, packages), key=comparator):
- print(package.pretty_print())
- print(f"\t{package.version}")
- print(f"\t{package_status.pretty_print()}")
+ PackagePrinter(package, package_status).print(args.info)
diff --git a/src/ahriman/application/handlers/user.py b/src/ahriman/application/handlers/user.py
index c1768624..7ecfa8b4 100644
--- a/src/ahriman/application/handlers/user.py
+++ b/src/ahriman/application/handlers/user.py
@@ -113,8 +113,7 @@ class User(Handler):
:param salt_length: salt length
:return: current salt
"""
- salt = configuration.get("auth", "salt", fallback=None)
- if salt:
+ if salt := configuration.get("auth", "salt", fallback=None):
return salt
return MUser.generate_password(salt_length)
diff --git a/src/ahriman/core/configuration.py b/src/ahriman/core/configuration.py
index 0cc1cb4c..874a47f9 100644
--- a/src/ahriman/core/configuration.py
+++ b/src/ahriman/core/configuration.py
@@ -175,7 +175,7 @@ class Configuration(configparser.RawConfigParser):
level=self.DEFAULT_LOG_LEVEL)
logging.exception("could not load logging from configuration, fallback to stderr")
if quiet:
- logging.disable()
+ logging.disable(logging.WARNING) # only print errors here
def merge_sections(self, architecture: str) -> None:
"""
diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py
index ac67c399..3509e532 100644
--- a/src/ahriman/core/repository/executor.py
+++ b/src/ahriman/core/repository/executor.py
@@ -138,17 +138,17 @@ class Executor(Cleaner):
:param packages: list of filenames to run
:return: path to repository database
"""
- def update_single(fn: Optional[str], base: str) -> None:
- if fn is None:
+ def update_single(name: Optional[str], base: str) -> None:
+ if name is None:
self.logger.warning("received empty package name for base %s", base)
return # suppress type checking, it never can be none actually
# in theory it might be NOT packages directory, but we suppose it is
- full_path = self.paths.packages / fn
+ full_path = self.paths.packages / name
files = self.sign.process_sign_package(full_path, base)
for src in files:
dst = self.paths.repository / src.name
shutil.move(src, dst)
- package_path = self.paths.repository / fn
+ package_path = self.paths.repository / name
self.repo.add(package_path)
# we are iterating over bases, not single packages
diff --git a/src/ahriman/core/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py
index 22bb0ac6..c9537b26 100644
--- a/src/ahriman/core/repository/update_handler.py
+++ b/src/ahriman/core/repository/update_handler.py
@@ -72,16 +72,16 @@ class UpdateHandler(Cleaner):
result: List[Package] = []
known_bases = {package.base for package in self.packages()}
- for fn in self.paths.manual.iterdir():
+ for filename in self.paths.manual.iterdir():
try:
- local = Package.load(fn, self.pacman, self.aur_url)
+ local = Package.load(filename, self.pacman, self.aur_url)
result.append(local)
if local.base not in known_bases:
self.reporter.set_unknown(local)
else:
self.reporter.set_pending(local.base)
except Exception:
- self.logger.exception("could not add package from %s", fn)
+ self.logger.exception("could not add package from %s", filename)
self.clear_manual()
return result
diff --git a/src/ahriman/core/status/watcher.py b/src/ahriman/core/status/watcher.py
index ad62ba76..d3c767ab 100644
--- a/src/ahriman/core/status/watcher.py
+++ b/src/ahriman/core/status/watcher.py
@@ -126,11 +126,10 @@ class Watcher:
"""
for package in self.repository.packages():
# get status of build or assign unknown
- current = self.known.get(package.base)
- if current is None:
- status = BuildStatus()
- else:
+ if (current := self.known.get(package.base)) is not None:
_, status = current
+ else:
+ status = BuildStatus()
self.known[package.base] = (package, status)
self._cache_load()
diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py
index 125619c5..bed5aa65 100644
--- a/src/ahriman/core/util.py
+++ b/src/ahriman/core/util.py
@@ -24,7 +24,7 @@ import requests
from logging import Logger
from pathlib import Path
-from typing import Generator, Optional, Union
+from typing import Any, Dict, Generator, Iterable, Optional, Union
from ahriman.core.exceptions import InvalidOption, UnsafeRun
@@ -78,6 +78,16 @@ def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
return result
+def filter_json(source: Dict[str, Any], known_fields: Iterable[str]) -> Dict[str, Any]:
+ """
+ filter json object by fields used for json-to-object conversion
+ :param source: raw json object
+ :param known_fields: list of fields which have to be known for the target object
+ :return: json object without unknown and empty fields
+ """
+ return {key: value for key, value in source.items() if key in known_fields and value is not None}
+
+
def package_like(filename: Path) -> bool:
"""
check if file looks like package
@@ -88,13 +98,17 @@ def package_like(filename: Path) -> bool:
return ".pkg." in name and not name.endswith(".sig")
-def pretty_datetime(timestamp: Optional[Union[float, int]]) -> str:
+def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -> str:
"""
convert datetime object to string
:param timestamp: datetime to convert
:return: pretty printable datetime as string
"""
- return "" if timestamp is None else datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
+ if timestamp is None:
+ return ""
+ if isinstance(timestamp, (int, float)):
+ timestamp = datetime.datetime.utcfromtimestamp(timestamp)
+ return timestamp.strftime("%Y-%m-%d %H:%M:%S")
def pretty_size(size: Optional[float], level: int = 0) -> str:
diff --git a/src/ahriman/models/build_status.py b/src/ahriman/models/build_status.py
index 1bd2bf93..262b8efc 100644
--- a/src/ahriman/models/build_status.py
+++ b/src/ahriman/models/build_status.py
@@ -21,10 +21,11 @@ from __future__ import annotations
import datetime
+from dataclasses import dataclass, fields
from enum import Enum
-from typing import Any, Dict, Optional, Type, Union
+from typing import Any, Dict, Type
-from ahriman.core.util import pretty_datetime
+from ahriman.core.util import filter_json, pretty_datetime
class BuildStatusEnum(Enum):
@@ -74,6 +75,7 @@ class BuildStatusEnum(Enum):
return "secondary"
+@dataclass
class BuildStatus:
"""
build status holder
@@ -81,15 +83,14 @@ class BuildStatus:
:ivar timestamp: build status update time
"""
- def __init__(self, status: Union[BuildStatusEnum, str, None] = None,
- timestamp: Optional[int] = None) -> None:
+ status: BuildStatusEnum = BuildStatusEnum.Unknown
+ timestamp: int = int(datetime.datetime.utcnow().timestamp())
+
+ def __post_init__(self) -> None:
"""
- default constructor
- :param status: current build status if known. `BuildStatusEnum.Unknown` will be used if not set
- :param timestamp: build status timestamp. Current timestamp will be used if not set
+ convert status to enum type
"""
- self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
- self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp())
+ self.status = BuildStatusEnum(self.status)
@classmethod
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:
@@ -98,7 +99,8 @@ class BuildStatus:
:param dump: json dump body
:return: status properties
"""
- return cls(dump.get("status"), dump.get("timestamp"))
+ known_fields = [pair.name for pair in fields(cls)]
+ return cls(**filter_json(dump, known_fields))
def pretty_print(self) -> str:
"""
@@ -116,20 +118,3 @@ class BuildStatus:
"status": self.status.value,
"timestamp": self.timestamp
}
-
- def __eq__(self, other: Any) -> bool:
- """
- compare object to other
- :param other: other object to compare
- :return: True in case if objects are equal
- """
- if not isinstance(other, BuildStatus):
- return False
- return self.status == other.status and self.timestamp == other.timestamp
-
- def __repr__(self) -> str:
- """
- generate string representation of object
- :return: unique string representation
- """
- return f"BuildStatus(status={self.status.value}, timestamp={self.timestamp})"
diff --git a/src/ahriman/models/counters.py b/src/ahriman/models/counters.py
index 13e7e344..5ae671dc 100644
--- a/src/ahriman/models/counters.py
+++ b/src/ahriman/models/counters.py
@@ -22,6 +22,7 @@ from __future__ import annotations
from dataclasses import dataclass, fields
from typing import Any, Dict, List, Tuple, Type
+from ahriman.core.util import filter_json
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
@@ -54,8 +55,7 @@ class Counters:
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
- dump = {key: value for key, value in dump.items() if key in known_fields}
- return cls(**dump)
+ return cls(**filter_json(dump, known_fields))
@classmethod
def from_packages(cls: Type[Counters], packages: List[Tuple[Package, BuildStatus]]) -> Counters:
diff --git a/src/ahriman/models/package_description.py b/src/ahriman/models/package_description.py
index dbd9ff6f..eb4d6bc4 100644
--- a/src/ahriman/models/package_description.py
+++ b/src/ahriman/models/package_description.py
@@ -24,6 +24,8 @@ from pathlib import Path
from pyalpm import Package # type: ignore
from typing import Any, Dict, List, Optional, Type
+from ahriman.core.util import filter_json
+
@dataclass
class PackageDescription:
@@ -70,8 +72,7 @@ class PackageDescription:
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
- dump = {key: value for key, value in dump.items() if key in known_fields}
- return cls(**dump)
+ return cls(**filter_json(dump, known_fields))
@classmethod
def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription:
diff --git a/src/ahriman/models/property.py b/src/ahriman/models/property.py
new file mode 100644
index 00000000..af6a0591
--- /dev/null
+++ b/src/ahriman/models/property.py
@@ -0,0 +1,31 @@
+# (see https://github.com/arcan1s/ahriman).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+from dataclasses import dataclass
+from typing import Any
+
+
+@dataclass
+class Property:
+ """
+ holder of object properties descriptor
+ :ivar name: name of the property
+ :ivar value: property value
+ :ivar is_required: if set to True then this property is required
+ """
+
+ name: str
+ value: Any
+ is_required: bool = False
diff --git a/src/ahriman/web/middlewares/auth_handler.py b/src/ahriman/web/middlewares/auth_handler.py
index da01c9b7..f2cbdc2c 100644
--- a/src/ahriman/web/middlewares/auth_handler.py
+++ b/src/ahriman/web/middlewares/auth_handler.py
@@ -81,8 +81,7 @@ def auth_handler() -> MiddlewareType:
"""
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
- permission_method = getattr(handler, "get_permission", None)
- if permission_method is not None:
+ if (permission_method := getattr(handler, "get_permission", None)) is not None:
permission = await permission_method(request)
elif isinstance(handler, types.MethodType): # additional wrapper for static resources
handler_instance = getattr(handler, "__self__", None)
diff --git a/src/ahriman/web/views/user/login.py b/src/ahriman/web/views/user/login.py
index 76593488..ef91aabc 100644
--- a/src/ahriman/web/views/user/login.py
+++ b/src/ahriman/web/views/user/login.py
@@ -45,11 +45,11 @@ class LoginView(BaseView):
"""
from ahriman.core.auth.oauth import OAuth
- code = self.request.query.getone("code", default=None)
oauth_provider = self.validator
if not isinstance(oauth_provider, OAuth): # there is actually property, but mypy does not like it anyway
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
+ code = self.request.query.getone("code", default=None)
if not code:
raise HTTPFound(oauth_provider.get_oauth_url())
diff --git a/tests/ahriman/application/formatters/conftest.py b/tests/ahriman/application/formatters/conftest.py
new file mode 100644
index 00000000..d6186288
--- /dev/null
+++ b/tests/ahriman/application/formatters/conftest.py
@@ -0,0 +1,47 @@
+import aur
+import pytest
+
+from ahriman.application.formatters.aur_printer import AurPrinter
+from ahriman.application.formatters.configuration_printer import ConfigurationPrinter
+from ahriman.application.formatters.package_printer import PackagePrinter
+from ahriman.application.formatters.status_printer import StatusPrinter
+from ahriman.models.build_status import BuildStatus
+from ahriman.models.package import Package
+
+
+@pytest.fixture
+def aur_package_ahriman_printer(aur_package_ahriman: aur.Package) -> AurPrinter:
+ """
+ fixture for AUR package printer
+ :param aur_package_ahriman: AUR package fixture
+ :return: AUR package printer test instance
+ """
+ return AurPrinter(aur_package_ahriman)
+
+
+@pytest.fixture
+def configuration_printer() -> ConfigurationPrinter:
+ """
+ fixture for configuration printer
+ :return: configuration printer test instance
+ """
+ return ConfigurationPrinter("section", {"key_one": "value_one", "key_two": "value_two"})
+
+
+@pytest.fixture
+def package_ahriman_printer(package_ahriman: Package) -> PackagePrinter:
+ """
+ fixture for package printer
+ :param package_ahriman: package fixture
+ :return: package printer test instance
+ """
+ return PackagePrinter(package_ahriman, BuildStatus())
+
+
+@pytest.fixture
+def status_printer(package_ahriman: Package) -> StatusPrinter:
+ """
+ fixture for build status printer
+ :return: build status printer test instance
+ """
+ return StatusPrinter(BuildStatus())
diff --git a/tests/ahriman/application/formatters/test_aur_printer.py b/tests/ahriman/application/formatters/test_aur_printer.py
new file mode 100644
index 00000000..8725dff6
--- /dev/null
+++ b/tests/ahriman/application/formatters/test_aur_printer.py
@@ -0,0 +1,15 @@
+from ahriman.application.formatters.aur_printer import AurPrinter
+
+
+def test_properties(aur_package_ahriman_printer: AurPrinter) -> None:
+ """
+ must return non empty properties list
+ """
+ assert aur_package_ahriman_printer.properties()
+
+
+def test_title(aur_package_ahriman_printer: AurPrinter) -> None:
+ """
+ must return non empty title
+ """
+ assert aur_package_ahriman_printer.title() is not None
diff --git a/tests/ahriman/application/formatters/test_configuration_printer.py b/tests/ahriman/application/formatters/test_configuration_printer.py
new file mode 100644
index 00000000..b7117d4a
--- /dev/null
+++ b/tests/ahriman/application/formatters/test_configuration_printer.py
@@ -0,0 +1,22 @@
+from ahriman.application.formatters.configuration_printer import ConfigurationPrinter
+
+
+def test_properties(configuration_printer: ConfigurationPrinter) -> None:
+ """
+ must return non empty properties list
+ """
+ assert configuration_printer.properties()
+
+
+def test_properties_required(configuration_printer: ConfigurationPrinter) -> None:
+ """
+ must return all properties as required
+ """
+ assert all(prop.is_required for prop in configuration_printer.properties())
+
+
+def test_title(configuration_printer: ConfigurationPrinter) -> None:
+ """
+ must return non empty title
+ """
+ assert configuration_printer.title() == "[section]"
diff --git a/tests/ahriman/application/formatters/test_package_printer.py b/tests/ahriman/application/formatters/test_package_printer.py
new file mode 100644
index 00000000..06525a6b
--- /dev/null
+++ b/tests/ahriman/application/formatters/test_package_printer.py
@@ -0,0 +1,15 @@
+from ahriman.application.formatters.package_printer import PackagePrinter
+
+
+def test_properties(package_ahriman_printer: PackagePrinter) -> None:
+ """
+ must return non empty properties list
+ """
+ assert package_ahriman_printer.properties()
+
+
+def test_title(package_ahriman_printer: PackagePrinter) -> None:
+ """
+ must return non empty title
+ """
+ assert package_ahriman_printer.title() is not None
diff --git a/tests/ahriman/application/formatters/test_printer.py b/tests/ahriman/application/formatters/test_printer.py
new file mode 100644
index 00000000..92b3d31f
--- /dev/null
+++ b/tests/ahriman/application/formatters/test_printer.py
@@ -0,0 +1,45 @@
+from unittest.mock import MagicMock
+
+from ahriman.application.formatters.package_printer import PackagePrinter
+from ahriman.application.formatters.printer import Printer
+
+
+def test_print(package_ahriman_printer: PackagePrinter) -> None:
+ """
+ must print content
+ """
+ log_mock = MagicMock()
+ package_ahriman_printer.print(verbose=False, log_fn=log_mock)
+ log_mock.assert_called()
+
+
+def test_print_empty() -> None:
+ """
+ must not print empty object
+ """
+ log_mock = MagicMock()
+ Printer().print(verbose=True, log_fn=log_mock)
+ log_mock.assert_not_called()
+
+
+def test_print_verbose(package_ahriman_printer: PackagePrinter) -> None:
+ """
+ must print content with increased verbosity
+ """
+ log_mock = MagicMock()
+ package_ahriman_printer.print(verbose=True, log_fn=log_mock)
+ log_mock.assert_called()
+
+
+def test_properties() -> None:
+ """
+ must return empty properties list
+ """
+ assert Printer().properties() == []
+
+
+def test_title() -> None:
+ """
+ must return empty title
+ """
+ assert Printer().title() is None
diff --git a/tests/ahriman/application/formatters/test_status_printer.py b/tests/ahriman/application/formatters/test_status_printer.py
new file mode 100644
index 00000000..b3026b9b
--- /dev/null
+++ b/tests/ahriman/application/formatters/test_status_printer.py
@@ -0,0 +1,15 @@
+from ahriman.application.formatters.status_printer import StatusPrinter
+
+
+def test_properties(status_printer: StatusPrinter) -> None:
+ """
+ must return empty properties list
+ """
+ assert not status_printer.properties()
+
+
+def test_title(status_printer: StatusPrinter) -> None:
+ """
+ must return non empty title
+ """
+ assert status_printer.title() is not None
diff --git a/tests/ahriman/application/handlers/test_handler_dump.py b/tests/ahriman/application/handlers/test_handler_dump.py
index 9c52bf01..b8de3c80 100644
--- a/tests/ahriman/application/handlers/test_handler_dump.py
+++ b/tests/ahriman/application/handlers/test_handler_dump.py
@@ -11,7 +11,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
must run command
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
- print_mock = mocker.patch("ahriman.application.handlers.dump.Dump._print")
+ print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print")
application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump",
return_value=configuration.dump())
diff --git a/tests/ahriman/application/handlers/test_handler_remove_unknown.py b/tests/ahriman/application/handlers/test_handler_remove_unknown.py
index 64d34376..51af2860 100644
--- a/tests/ahriman/application/handlers/test_handler_remove_unknown.py
+++ b/tests/ahriman/application/handlers/test_handler_remove_unknown.py
@@ -14,6 +14,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
:return: generated arguments for these test cases
"""
args.dry_run = False
+ args.info = False
return args
@@ -42,19 +43,29 @@ def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, pac
application_mock = mocker.patch("ahriman.application.application.Application.unknown",
return_value=[package_ahriman])
remove_mock = mocker.patch("ahriman.application.application.Application.remove")
- log_fn_mock = mocker.patch("ahriman.application.handlers.remove_unknown.RemoveUnknown.log_fn")
+ print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print")
RemoveUnknown.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
remove_mock.assert_not_called()
- log_fn_mock.assert_called_once_with(package_ahriman)
+ print_mock.assert_called_once_with(False)
-def test_log_fn(package_ahriman: Package, mocker: MockerFixture) -> None:
+def test_run_dry_run_verbose(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
"""
- log function must call print built-in
+ must run simplified command with increased verbosity
"""
- print_mock = mocker.patch("builtins.print")
+ args = _default_args(args)
+ args.dry_run = True
+ args.info = True
+ mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
+ application_mock = mocker.patch("ahriman.application.application.Application.unknown",
+ return_value=[package_ahriman])
+ remove_mock = mocker.patch("ahriman.application.application.Application.remove")
+ print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print")
- RemoveUnknown.log_fn(package_ahriman)
- print_mock.assert_called() # we don't really care about call details tbh
+ RemoveUnknown.run(args, "x86_64", configuration, True)
+ application_mock.assert_called_once()
+ remove_mock.assert_not_called()
+ print_mock.assert_called_once_with(True)
diff --git a/tests/ahriman/application/handlers/test_handler_search.py b/tests/ahriman/application/handlers/test_handler_search.py
index aff5d29f..fc836c86 100644
--- a/tests/ahriman/application/handlers/test_handler_search.py
+++ b/tests/ahriman/application/handlers/test_handler_search.py
@@ -1,10 +1,13 @@
import argparse
import aur
+import pytest
from pytest_mock import MockerFixture
+from unittest import mock
from ahriman.application.handlers import Search
from ahriman.core.configuration import Configuration
+from ahriman.core.exceptions import InvalidOption
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
@@ -14,6 +17,8 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
:return: generated arguments for these test cases
"""
args.search = ["ahriman"]
+ args.info = False
+ args.sort_by = "name"
return args
@@ -24,35 +29,71 @@ def test_run(args: argparse.Namespace, configuration: Configuration, aur_package
"""
args = _default_args(args)
mocker.patch("aur.search", return_value=[aur_package_ahriman])
- log_mock = mocker.patch("ahriman.application.handlers.search.Search.log_fn")
+ print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print")
Search.run(args, "x86_64", configuration, True)
- log_mock.assert_called_once()
+ print_mock.assert_called_once()
-def test_run_multiple_search(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+def test_run_multiple_search(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
+ mocker: MockerFixture) -> None:
"""
must run command with multiple search arguments
"""
args = _default_args(args)
args.search = ["ahriman", "is", "cool"]
- search_mock = mocker.patch("aur.search")
+ search_mock = mocker.patch("aur.search", return_value=[aur_package_ahriman])
Search.run(args, "x86_64", configuration, True)
- search_mock.assert_called_once_with(" ".join(args.search))
+ search_mock.assert_has_calls([mock.call(term) for term in args.search])
-def test_log_fn(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
- mocker: MockerFixture) -> None:
+def test_run_sort(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
+ mocker: MockerFixture) -> None:
"""
- log function must call print built-in
+ must run command with sorting
"""
args = _default_args(args)
mocker.patch("aur.search", return_value=[aur_package_ahriman])
- print_mock = mocker.patch("builtins.print")
+ sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort")
Search.run(args, "x86_64", configuration, True)
- print_mock.assert_called() # we don't really care about call details tbh
+ sort_mock.assert_called_once_with([aur_package_ahriman], "name")
+
+
+def test_run_sort_by(args: argparse.Namespace, configuration: Configuration, aur_package_ahriman: aur.Package,
+ mocker: MockerFixture) -> None:
+ """
+ must run command with sorting by specified field
+ """
+ args = _default_args(args)
+ args.sort_by = "field"
+ mocker.patch("aur.search", return_value=[aur_package_ahriman])
+ sort_mock = mocker.patch("ahriman.application.handlers.search.Search.sort")
+
+ Search.run(args, "x86_64", configuration, True)
+ sort_mock.assert_called_once_with([aur_package_ahriman], "field")
+
+
+def test_sort(aur_package_ahriman: aur.Package) -> None:
+ """
+ must sort package list
+ """
+ another = aur_package_ahriman._replace(name="1", package_base="base")
+ # sort by name
+ assert Search.sort([aur_package_ahriman, another], "name") == [another, aur_package_ahriman]
+ # sort by another field
+ assert Search.sort([aur_package_ahriman, another], "package_base") == [aur_package_ahriman, another]
+ # sort by field with the same values
+ assert Search.sort([aur_package_ahriman, another], "version") == [another, aur_package_ahriman]
+
+
+def test_sort_exception(aur_package_ahriman: aur.Package) -> None:
+ """
+ must raise an exception on unknown sorting field
+ """
+ with pytest.raises(InvalidOption):
+ Search.sort([aur_package_ahriman], "random_field")
def test_disallow_auto_architecture_run() -> None:
@@ -60,3 +101,10 @@ def test_disallow_auto_architecture_run() -> None:
must not allow multi architecture run
"""
assert not Search.ALLOW_AUTO_ARCHITECTURE_RUN
+
+
+def test_sort_fields() -> None:
+ """
+ must store valid field list which are allowed to be used for sorting
+ """
+ assert all(field in aur.Package._fields for field in Search.SORT_FIELDS)
diff --git a/tests/ahriman/application/handlers/test_handler_status.py b/tests/ahriman/application/handlers/test_handler_status.py
index d31722bc..791f697d 100644
--- a/tests/ahriman/application/handlers/test_handler_status.py
+++ b/tests/ahriman/application/handlers/test_handler_status.py
@@ -1,6 +1,7 @@
import argparse
from pytest_mock import MockerFixture
+from unittest import mock
from ahriman.application.handlers import Status
from ahriman.core.configuration import Configuration
@@ -15,6 +16,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
:return: generated arguments for these test cases
"""
args.ahriman = True
+ args.info = False
args.package = []
args.status = None
return args
@@ -31,12 +33,28 @@ def test_run(args: argparse.Namespace, configuration: Configuration, package_ahr
packages_mock = mocker.patch("ahriman.core.status.client.Client.get",
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed))])
- pretty_print_mock = mocker.patch("ahriman.models.package.Package.pretty_print")
+ print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print")
Status.run(args, "x86_64", configuration, True)
application_mock.assert_called_once()
packages_mock.assert_called_once()
- pretty_print_mock.assert_called()
+ print_mock.assert_has_calls([mock.call(False) for _ in range(3)])
+
+
+def test_run_verbose(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ args = _default_args(args)
+ args.info = True
+ mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
+ mocker.patch("ahriman.core.status.client.Client.get",
+ return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success))])
+ print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print")
+
+ Status.run(args, "x86_64", configuration, True)
+ print_mock.assert_has_calls([mock.call(True) for _ in range(2)])
def test_run_with_package_filter(args: argparse.Namespace, configuration: Configuration, package_ahriman: Package,
@@ -65,10 +83,10 @@ def test_run_by_status(args: argparse.Namespace, configuration: Configuration, p
return_value=[(package_ahriman, BuildStatus(BuildStatusEnum.Success)),
(package_python_schedule, BuildStatus(BuildStatusEnum.Failed))])
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
- pretty_print_mock = mocker.patch("ahriman.models.package.Package.pretty_print")
+ print_mock = mocker.patch("ahriman.application.formatters.printer.Printer.print")
Status.run(args, "x86_64", configuration, True)
- pretty_print_mock.assert_called_once()
+ print_mock.assert_has_calls([mock.call(False) for _ in range(2)])
def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
diff --git a/tests/ahriman/core/test_configuration.py b/tests/ahriman/core/test_configuration.py
index 7c1695ff..1da9e74f 100644
--- a/tests/ahriman/core/test_configuration.py
+++ b/tests/ahriman/core/test_configuration.py
@@ -1,4 +1,5 @@
import configparser
+import logging
import pytest
from pathlib import Path
@@ -194,7 +195,7 @@ def test_load_logging_quiet(configuration: Configuration, mocker: MockerFixture)
"""
disable_mock = mocker.patch("logging.disable")
configuration.load_logging(quiet=True)
- disable_mock.assert_called_once()
+ disable_mock.assert_called_once_with(logging.WARNING)
def test_merge_sections_missing(configuration: Configuration) -> None:
diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py
index 44d2f7ce..432e4a7f 100644
--- a/tests/ahriman/core/test_util.py
+++ b/tests/ahriman/core/test_util.py
@@ -1,3 +1,4 @@
+import datetime
import logging
import pytest
import subprocess
@@ -6,7 +7,7 @@ from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.exceptions import InvalidOption, UnsafeRun
-from ahriman.core.util import check_output, check_user, package_like, pretty_datetime, pretty_size, walk
+from ahriman.core.util import check_output, check_user, filter_json, package_like, pretty_datetime, pretty_size, walk
from ahriman.models.package import Package
@@ -79,6 +80,26 @@ def test_check_user_exception(mocker: MockerFixture) -> None:
check_user(cwd)
+def test_filter_json(package_ahriman: Package) -> None:
+ """
+ must filter fields by known list
+ """
+ expected = package_ahriman.view()
+ probe = package_ahriman.view()
+ probe["unknown_field"] = "value"
+
+ assert expected == filter_json(probe, expected.keys())
+
+
+def test_filter_json_empty_value(package_ahriman: Package) -> None:
+ """
+ must return empty values from object
+ """
+ probe = package_ahriman.view()
+ probe["base"] = None
+ assert "base" not in filter_json(probe, probe.keys())
+
+
def test_package_like(package_ahriman: Package) -> None:
"""
package_like must return true for archives
@@ -102,6 +123,13 @@ def test_pretty_datetime() -> None:
assert pretty_datetime(0) == "1970-01-01 00:00:00"
+def test_pretty_datetime_datetime() -> None:
+ """
+ must generate string from datetime object
+ """
+ assert pretty_datetime(datetime.datetime(1970, 1, 1, 0, 0, 0)) == "1970-01-01 00:00:00"
+
+
def test_pretty_datetime_empty() -> None:
"""
must generate empty string from None timestamp
diff --git a/tests/ahriman/models/test_property.py b/tests/ahriman/models/test_property.py
new file mode 100644
index 00000000..e69de29b