mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
Add ability to show more info in search and status subcommands
This feature also introduces the followiing changes * aur-search command now works as expected with multiterms * printer classes for managing of data print * --sort-by argument for aur-search subcommand instead of using package name * --quiet argument now has also --no-quite option * if --quite is supplied, the log level will be set to warn instead of critical to be able to see error messages * pretty_datetime function now also supports datetime objects * BuildStatus is now pure dataclass
This commit is contained in:
parent
9057ecf67a
commit
7d782f120d
@ -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
|
||||
|
11
docs/faq.md
11
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:
|
||||
|
@ -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
|
||||
|
||||
|
19
src/ahriman/application/formatters/__init__.py
Normal file
19
src/ahriman/application/formatters/__init__.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
62
src/ahriman/application/formatters/aur_printer.py
Normal file
62
src/ahriman/application/formatters/aur_printer.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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})"
|
55
src/ahriman/application/formatters/configuration_printer.py
Normal file
55
src/ahriman/application/formatters/configuration_printer.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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}]"
|
60
src/ahriman/application/formatters/package_printer.py
Normal file
60
src/ahriman/application/formatters/package_printer.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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()
|
55
src/ahriman/application/formatters/printer.py
Normal file
55
src/ahriman/application/formatters/printer.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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
|
||||
"""
|
51
src/ahriman/application/formatters/status_printer.py
Normal file
51
src/ahriman/application/formatters/status_printer.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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()
|
@ -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=" = ")
|
||||
|
@ -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}")
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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})"
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
31
src/ahriman/models/property.py
Normal file
31
src/ahriman/models/property.py
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
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
|
@ -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)
|
||||
|
@ -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())
|
||||
|
||||
|
47
tests/ahriman/application/formatters/conftest.py
Normal file
47
tests/ahriman/application/formatters/conftest.py
Normal file
@ -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())
|
15
tests/ahriman/application/formatters/test_aur_printer.py
Normal file
15
tests/ahriman/application/formatters/test_aur_printer.py
Normal file
@ -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
|
@ -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]"
|
15
tests/ahriman/application/formatters/test_package_printer.py
Normal file
15
tests/ahriman/application/formatters/test_package_printer.py
Normal file
@ -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
|
45
tests/ahriman/application/formatters/test_printer.py
Normal file
45
tests/ahriman/application/formatters/test_printer.py
Normal file
@ -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
|
15
tests/ahriman/application/formatters/test_status_printer.py
Normal file
15
tests/ahriman/application/formatters/test_status_printer.py
Normal file
@ -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
|
@ -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())
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
0
tests/ahriman/models/test_property.py
Normal file
0
tests/ahriman/models/test_property.py
Normal file
Loading…
Reference in New Issue
Block a user