From d7966e419d3cfb7ce98512da8eb0958fb1ceecb9 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Mon, 23 May 2022 19:10:26 +0300 Subject: [PATCH] add shell and version parser --- .github/ISSUE_TEMPLATE/bug-report.md | 10 ++- .github/ISSUE_TEMPLATE/feature-request.md | 2 +- docs/ahriman.1 | 22 ++++- docs/ahriman.application.handlers.rst | 16 ++++ docs/ahriman.core.formatters.rst | 8 ++ docs/conf.py | 1 - package/share/ahriman/templates/shell | 18 ++++ setup.py | 1 + src/ahriman/application/ahriman.py | 54 ++++++++++-- src/ahriman/application/handlers/__init__.py | 2 + src/ahriman/application/handlers/shell.py | 59 +++++++++++++ src/ahriman/application/handlers/versions.py | 87 +++++++++++++++++++ src/ahriman/core/formatters/__init__.py | 1 + src/ahriman/core/formatters/string_printer.py | 3 + .../core/formatters/version_printer.py | 55 ++++++++++++ src/ahriman/core/util.py | 38 ++++++-- .../handlers/test_handler_shell.py | 48 ++++++++++ .../handlers/test_handler_versions.py | 38 ++++++++ tests/ahriman/application/test_ahriman.py | 29 +++++++ tests/ahriman/core/formatters/conftest.py | 17 +++- .../core/formatters/test_version_printer.py | 15 ++++ tests/ahriman/core/test_util.py | 13 ++- 22 files changed, 511 insertions(+), 26 deletions(-) create mode 100644 package/share/ahriman/templates/shell create mode 100644 src/ahriman/application/handlers/shell.py create mode 100644 src/ahriman/application/handlers/versions.py create mode 100644 src/ahriman/core/formatters/version_printer.py create mode 100644 tests/ahriman/application/handlers/test_handler_shell.py create mode 100644 tests/ahriman/application/handlers/test_handler_versions.py create mode 100644 tests/ahriman/core/formatters/test_version_printer.py diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 29153f57..6b509e43 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -11,9 +11,9 @@ assignees: '' A clear and concise description of what the bug is. -### Steps to Reproduce +### Steps to reproduce -Steps to reproduce the behavior (commands, environment etc) +Steps to reproduce the behavior (commands, environment etc). ### Expected behavior @@ -21,4 +21,8 @@ A clear and concise description of what you expected to happen. ### Logs -Add logs to help explain your problem. Logs to stderr can be generated by using `--no-log` command line option. +Add logs to help explain your problem. By default, the application writes logs into `/dev/log` which is usually default systemd journal and can be accessed by `journalctl` command. + +You can also attach any additional information which can be helpful, e.g. configuration used by the application (be aware of passwords and other secrets if any); it can be generated by using `ahriman config` command. + +It is also sometimes useful to have information about installed packages which can be accessed by `ahriman version` command. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 63406d5b..b6daddd1 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -13,7 +13,7 @@ Brief description of the feature required ### Cause of the feature request -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +A clear and concise description of what the problem is. E.g. I'm always frustrated when [...] ### Proposed changes and/or features diff --git a/docs/ahriman.1 b/docs/ahriman.1 index 8f690230..c17a063e 100644 --- a/docs/ahriman.1 +++ b/docs/ahriman.1 @@ -3,9 +3,9 @@ ahriman .SH SYNOPSIS .B ahriman -[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-v] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,user-add,user-list,user-remove,web} ... +[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,key-import,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,repo-backup,repo-check,check,repo-clean,clean,repo-config,config,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-setup,init,repo-init,setup,repo-sign,sign,repo-status-update,repo-sync,sync,repo-triggers,repo-update,update,shell,user-add,user-list,user-remove,version,web} ... .SH DESCRIPTION -ArcH Linux ReposItory MANager +ArcH linux ReposItory MANager .SH OPTIONS .TP @@ -37,7 +37,7 @@ force disable any logging allow to run ahriman as non\-ahriman user. Some actions might be unavailable .TP -\fB\-v\fR, \fB\-\-version\fR +\fB\-V\fR, \fB\-\-version\fR show program's version number and exit .SH @@ -121,6 +121,9 @@ run triggers \fBahriman\fR \fI\,repo-update\/\fR update packages .TP +\fBahriman\fR \fI\,shell\/\fR +envoke python shell +.TP \fBahriman\fR \fI\,user-add\/\fR create or update user .TP @@ -130,6 +133,9 @@ user known users and their access \fBahriman\fR \fI\,user-remove\/\fR remove user .TP +\fBahriman\fR \fI\,version\/\fR +application version +.TP \fBahriman\fR \fI\,web\/\fR web server .SH COMMAND \fI\,'ahriman aur-search'\/\fR @@ -544,6 +550,11 @@ do not include manual updates \fB\-\-no\-vcs\fR do not check VCS packages +.SH COMMAND \fI\,'ahriman shell'\/\fR +usage: ahriman shell [-h] + +drop into python shell while having created application + .SH COMMAND \fI\,'ahriman user-add'\/\fR usage: ahriman user-add [-h] [--as-service] [-p PASSWORD] [-r {UserAccess.Unauthorized,UserAccess.Read,UserAccess.Reporter,UserAccess.Full}] [-s] @@ -606,6 +617,11 @@ username for web service \fB\-s\fR, \fB\-\-secure\fR set file permissions to user\-only +.SH COMMAND \fI\,'ahriman version'\/\fR +usage: ahriman version [-h] + +print application and its dependencies versions + .SH COMMAND \fI\,'ahriman web'\/\fR usage: ahriman web [-h] diff --git a/docs/ahriman.application.handlers.rst b/docs/ahriman.application.handlers.rst index 2a9976e2..889a85a1 100644 --- a/docs/ahriman.application.handlers.rst +++ b/docs/ahriman.application.handlers.rst @@ -116,6 +116,14 @@ ahriman.application.handlers.setup module :no-undoc-members: :show-inheritance: +ahriman.application.handlers.shell module +----------------------------------------- + +.. automodule:: ahriman.application.handlers.shell + :members: + :no-undoc-members: + :show-inheritance: + ahriman.application.handlers.sign module ---------------------------------------- @@ -172,6 +180,14 @@ ahriman.application.handlers.users module :no-undoc-members: :show-inheritance: +ahriman.application.handlers.versions module +-------------------------------------------- + +.. automodule:: ahriman.application.handlers.versions + :members: + :no-undoc-members: + :show-inheritance: + ahriman.application.handlers.web module --------------------------------------- diff --git a/docs/ahriman.core.formatters.rst b/docs/ahriman.core.formatters.rst index 528cb7a9..c3ecda6d 100644 --- a/docs/ahriman.core.formatters.rst +++ b/docs/ahriman.core.formatters.rst @@ -76,6 +76,14 @@ ahriman.core.formatters.user\_printer module :no-undoc-members: :show-inheritance: +ahriman.core.formatters.version\_printer module +----------------------------------------------- + +.. automodule:: ahriman.core.formatters.version_printer + :members: + :no-undoc-members: + :show-inheritance: + Module contents --------------- diff --git a/docs/conf.py b/docs/conf.py index 06882281..9df1ccff 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -104,5 +104,4 @@ autodoc_member_order = "groupwise" autodoc_default_options = { "no-undoc-members": True, - "special-members": "__init__", } diff --git a/package/share/ahriman/templates/shell b/package/share/ahriman/templates/shell new file mode 100644 index 00000000..0e851628 --- /dev/null +++ b/package/share/ahriman/templates/shell @@ -0,0 +1,18 @@ +  + ▄▄▄ ▄▄▄▄▄▄█▀  + ▄▄▄▄▄▄▄▄▄██▄▄▄█▄▄  + ██▄▄███▄▄▄▄▄██▄▄█▄▄  + █▄██████▄▄▄████▄▄█▄▄ ▄ + █▄▄▄█████████▄▄▄▄▀▄█▄█▀ + █▄▀▄████▄█▄▄▄▄███▄▄ ▀▀  + █▄▄▄████▄██████████  + ▄▄▄▄▄▄▄▄▄ ▀█▄█████▄▄▄▄█▄███▄  + ▄▄███▄▄▄▄▄▄▄▄▄ ▀ ▀▄█████▄▄█▄███  + ███▄▄████▄▄█▄██▄▄▄ ███▄▄▄▄▄▄█▀▀  + ███████▄▀ ▄▄▄██▄▄▄█████  + ██▄▄████ █▄█▄▄█████████  + ▄▄█▄▄██▄▀ ▀▄████▄██▄██▄▄  + ████████ ▄███▄▄▄█▄████  + ▄▄▄▄███▄▀ ▄▄█████ ███▄▄▄▄  + ▄▄▄██▄▄█▄▄▄▀ █████▄█ █████▄█  + ▀▀▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀  diff --git a/setup.py b/setup.py index 772b9b6b..eaf3b083 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ setup( "package/share/ahriman/templates/build-status.jinja2", "package/share/ahriman/templates/email-index.jinja2", "package/share/ahriman/templates/repo-index.jinja2", + "package/share/ahriman/templates/shell", "package/share/ahriman/templates/telegram-index.jinja2", ]), ("share/ahriman/templates/build-status", [ diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index f7c4de67..726223e5 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -26,6 +26,7 @@ from typing import List, TypeVar from ahriman import version from ahriman.application import handlers +from ahriman.core.util import enum_values from ahriman.models.action import Action from ahriman.models.build_status import BuildStatusEnum from ahriman.models.package_source import PackageSource @@ -76,7 +77,7 @@ def _parser() -> argparse.ArgumentParser: parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true") 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__) + parser.add_argument("-V", "--version", action="version", version=version.__version__) subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True) @@ -106,9 +107,11 @@ def _parser() -> argparse.ArgumentParser: _set_repo_sync_parser(subparsers) _set_repo_triggers_parser(subparsers) _set_repo_update_parser(subparsers) + _set_shell_parser(subparsers) _set_user_add_parser(subparsers) _set_user_list_parser(subparsers) _set_user_remove_parser(subparsers) + _set_version_parser(subparsers) _set_web_parser(subparsers) return parser @@ -225,7 +228,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") parser.add_argument("-n", "--now", help="run update function after", action="store_true") parser.add_argument("-s", "--source", help="explicitly specify the package source for this command", - type=PackageSource, choices=PackageSource, default=PackageSource.Auto) + type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto) parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true") parser.set_defaults(handler=handlers.Add) return parser @@ -267,7 +270,7 @@ def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", 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) + type=BuildStatusEnum, choices=enum_values(BuildStatusEnum)) parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, quiet=True, unsafe=True) return parser @@ -309,7 +312,7 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen "If no packages supplied, service status will be updated", nargs="*") parser.add_argument("-s", "--status", help="new status", - type=BuildStatusEnum, choices=BuildStatusEnum, default=BuildStatusEnum.Success) + type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success) parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, quiet=True, unsafe=True) return parser @@ -556,7 +559,7 @@ def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("--repository", help="repository name", required=True) parser.add_argument("--sign-key", help="sign key id") parser.add_argument("--sign-target", help="sign options", action="append", - type=SignSettings.from_option, choices=SignSettings) + type=SignSettings.from_option, choices=enum_values(SignSettings)) parser.add_argument("--web-port", help="port of the web service", type=int) parser.set_defaults(handler=handlers.Setup, lock=None, no_report=True, quiet=True, unsafe=True) return parser @@ -594,7 +597,7 @@ def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentPa parser = root.add_parser("repo-status-update", help="update repository status", description="update repository status on the status page", formatter_class=_formatter) parser.add_argument("-s", "--status", help="new status", - type=BuildStatusEnum, choices=BuildStatusEnum, default=BuildStatusEnum.Success) + type=BuildStatusEnum, choices=enum_values(BuildStatusEnum), default=BuildStatusEnum.Success) parser.set_defaults(handler=handlers.StatusUpdate, action=Action.Update, lock=None, no_report=True, package=[], quiet=True, unsafe=True) return parser @@ -661,6 +664,24 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser: return parser +def _set_shell_parser(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for shell subcommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("shell", help="envoke python shell", + description="drop into python shell while having created application", + formatter_class=_formatter) + parser.add_argument("-v", "--verbose", help=argparse.SUPPRESS, action="store_true") + parser.set_defaults(handler=handlers.Shell, lock=None, no_report=True) + return parser + + def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser: """ add parser for create user subcommand @@ -680,7 +701,7 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser: parser.add_argument("-p", "--password", help="user password. Blank password will be treated as empty password, " "which is in particular must be used for OAuth2 authorization type.") parser.add_argument("-r", "--role", help="user access level", - type=UserAccess, choices=UserAccess, default=UserAccess.Read) + type=UserAccess, choices=enum_values(UserAccess), default=UserAccess.Read) parser.add_argument("-s", "--secure", help="set file permissions to user-only", action="store_true") parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture=[""], lock=None, no_report=True, quiet=True, unsafe=True) @@ -702,7 +723,7 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser: formatter_class=_formatter) parser.add_argument("username", help="filter users by username", nargs="?") parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true") - parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=UserAccess) + parser.add_argument("-r", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess)) parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, no_report=True, # nosec password="", quiet=True, unsafe=True) return parser @@ -728,6 +749,23 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser: return parser +def _set_version_parser(root: SubParserAction) -> argparse.ArgumentParser: + """ + add parser for version subcommand + + Args: + root(SubParserAction): subparsers for the commands + + Returns: + argparse.ArgumentParser: created argument parser + """ + parser = root.add_parser("version", help="application version", + description="print application and its dependencies versions", formatter_class=_formatter) + parser.set_defaults(handler=handlers.Versions, architecture=[""], lock=None, no_report=True, quiet=True, + unsafe=True) + return parser + + def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser: """ add parser for web subcommand diff --git a/src/ahriman/application/handlers/__init__.py b/src/ahriman/application/handlers/__init__.py index 4d78ed17..6c72ad46 100644 --- a/src/ahriman/application/handlers/__init__.py +++ b/src/ahriman/application/handlers/__init__.py @@ -32,6 +32,7 @@ from ahriman.application.handlers.remove_unknown import RemoveUnknown from ahriman.application.handlers.restore import Restore from ahriman.application.handlers.search import Search from ahriman.application.handlers.setup import Setup +from ahriman.application.handlers.shell import Shell from ahriman.application.handlers.sign import Sign from ahriman.application.handlers.status import Status from ahriman.application.handlers.status_update import StatusUpdate @@ -39,4 +40,5 @@ from ahriman.application.handlers.triggers import Triggers from ahriman.application.handlers.unsafe_commands import UnsafeCommands from ahriman.application.handlers.update import Update from ahriman.application.handlers.users import Users +from ahriman.application.handlers.versions import Versions from ahriman.application.handlers.web import Web diff --git a/src/ahriman/application/handlers/shell.py b/src/ahriman/application/handlers/shell.py new file mode 100644 index 00000000..80149032 --- /dev/null +++ b/src/ahriman/application/handlers/shell.py @@ -0,0 +1,59 @@ +# +# Copyright (c) 2021-2022 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 argparse +import code +import sys + +from pathlib import Path +from typing import Type + +from ahriman.application.application import Application +from ahriman.application.handlers import Handler +from ahriman.core.configuration import Configuration +from ahriman.core.formatters import StringPrinter + + +class Shell(Handler): + """ + python shell handler + """ + + ALLOW_MULTI_ARCHITECTURE_RUN = False + + @classmethod + def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, + configuration: Configuration, no_report: bool, unsafe: bool) -> None: + """ + callback for command line + + Args: + args(argparse.Namespace): command line args + architecture(str): repository architecture + configuration(Configuration): configuration instance + no_report(bool): force disable reporting + unsafe(bool): if set no user check will be performed before path creation + """ + # pylint: disable=possibly-unused-variable + application = Application(architecture, configuration, no_report, unsafe) + if args.verbose: + # licensed by https://creativecommons.org/licenses/by-sa/3.0 + path = Path(sys.prefix) / "share" / "ahriman" / "templates" / "shell" + StringPrinter(path.read_text(encoding="utf8")).print(verbose=False) + code.interact(local=locals()) diff --git a/src/ahriman/application/handlers/versions.py b/src/ahriman/application/handlers/versions.py new file mode 100644 index 00000000..e849198e --- /dev/null +++ b/src/ahriman/application/handlers/versions.py @@ -0,0 +1,87 @@ +# +# Copyright (c) 2021-2022 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 argparse +import pkg_resources +import sys + +from typing import Dict, List, Tuple, Type + +from ahriman import version +from ahriman.application.handlers import Handler +from ahriman.core.configuration import Configuration +from ahriman.core.formatters import VersionPrinter + + +class Versions(Handler): + """ + version handler + """ + + ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture" + + @classmethod + def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, + configuration: Configuration, no_report: bool, unsafe: bool) -> None: + """ + callback for command line + + Args: + args(argparse.Namespace): command line args + architecture(str): repository architecture + configuration(Configuration): configuration instance + no_report(bool): force disable reporting + unsafe(bool): if set no user check will be performed before path creation + """ + VersionPrinter(f"Module version {version.__version__}", + {"Python": sys.version}).print(verbose=False, separator=" ") + packages = Versions.package_dependencies("ahriman", ("pacman", "s3", "web")) + VersionPrinter("Installed packages", packages).print(verbose=False, separator=" ") + + @staticmethod + def package_dependencies(root: str, root_extras: Tuple[str, ...] = ()) -> Dict[str, str]: + """ + extract list of ahriman package dependencies installed into system with their versions + + Args: + root(str): root package name + root_extras(Tuple[str, ...]): extras for the root package (Default value = ()) + + Returns: + Dict[str, str]: map of installed dependency to its version + """ + resources: Dict[str, pkg_resources.Distribution] = pkg_resources.working_set.by_key # type: ignore + + def dependencies_by_key(key: str, extras: Tuple[str, ...] = ()) -> List[str]: + return [entry.key for entry in resources[key].requires(extras)] + + keys: List[str] = [] + portion = {key for key in dependencies_by_key(root, root_extras) if key in resources} + while portion: + keys.extend(portion) + portion = { + key + for key in sum([dependencies_by_key(key) for key in portion], start=[]) + if key not in keys and key in resources + } + + return { + resource.project_name: resource.version + for resource in map(lambda key: resources[key], keys) + } diff --git a/src/ahriman/core/formatters/__init__.py b/src/ahriman/core/formatters/__init__.py index fea8b9d5..931e99d5 100644 --- a/src/ahriman/core/formatters/__init__.py +++ b/src/ahriman/core/formatters/__init__.py @@ -27,3 +27,4 @@ from ahriman.core.formatters.package_printer import PackagePrinter from ahriman.core.formatters.status_printer import StatusPrinter from ahriman.core.formatters.update_printer import UpdatePrinter from ahriman.core.formatters.user_printer import UserPrinter +from ahriman.core.formatters.version_printer import VersionPrinter diff --git a/src/ahriman/core/formatters/string_printer.py b/src/ahriman/core/formatters/string_printer.py index d9763f7b..70466bfe 100644 --- a/src/ahriman/core/formatters/string_printer.py +++ b/src/ahriman/core/formatters/string_printer.py @@ -25,6 +25,9 @@ from ahriman.core.formatters import Printer class StringPrinter(Printer): """ print content of the random string + + Attributes: + content(str): any content string """ def __init__(self, content: str) -> None: diff --git a/src/ahriman/core/formatters/version_printer.py b/src/ahriman/core/formatters/version_printer.py new file mode 100644 index 00000000..d2b0829a --- /dev/null +++ b/src/ahriman/core/formatters/version_printer.py @@ -0,0 +1,55 @@ +# +# Copyright (c) 2021-2022 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 + +from ahriman.core.formatters import StringPrinter +from ahriman.models.property import Property + + +class VersionPrinter(StringPrinter): + """ + print content of the python package versions + + Attributes: + packages(Dict[str, str]): map of package name to its version + """ + + def __init__(self, title: str, packages: Dict[str, str]) -> None: + """ + default constructor + + Args: + title(str): title of the message + packages(Dict[str, str]): map of package name to its version + """ + StringPrinter.__init__(self, title) + self.packages = packages + + def properties(self) -> List[Property]: + """ + convert content into printable data + + Returns: + List[Property]: list of content properties + """ + return [ + Property(package, version, is_required=True) + for package, version in sorted(self.packages.items()) + ] diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py index df9e6485..40bef688 100644 --- a/src/ahriman/core/util.py +++ b/src/ahriman/core/util.py @@ -18,7 +18,10 @@ # along with this program. If not, see . # import datetime +import io import os +from enum import Enum + import requests import shutil import subprocess @@ -27,14 +30,14 @@ import tempfile from contextlib import contextmanager from logging import Logger from pathlib import Path -from typing import Any, Dict, Generator, Iterable, List, Optional, Union +from typing import Any, Dict, Generator, IO, Iterable, List, Optional, Type, Union from ahriman.core.exceptions import InvalidOption, UnsafeRun from ahriman.models.repository_paths import RepositoryPaths -__all__ = ["check_output", "check_user", "exception_response_text", "filter_json", "full_version", "package_like", - "pretty_datetime", "pretty_size", "tmpdir", "walk"] +__all__ = ["check_output", "check_user", "exception_response_text", "filter_json", "full_version", "enum_values", + "package_like", "pretty_datetime", "pretty_size", "tmpdir", "walk"] def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] = None, @@ -73,6 +76,11 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] >>> check_output("false", exception=RuntimeError("An exception occurred")) """ + # hack for Optional[IO[str]] handle + def get_io(proc: subprocess.Popen[str], channel_name: str) -> IO[str]: + channel: Optional[IO[str]] = getattr(proc, channel_name, None) + return channel if channel is not None else io.StringIO() + def log(single: str) -> None: if logger is not None: logger.debug(single) @@ -80,14 +88,15 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] # FIXME additional workaround for linter and type check which do not know that user arg is supported # pylint: disable=unexpected-keyword-arg with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - user=user, text=True, encoding="utf8", bufsize=1) as process: # type: ignore + user=user, text=True, encoding="utf8", bufsize=1) as process: if input_data is not None: - process.stdin.write(input_data) - process.stdin.close() + input_channel = get_io(process, "stdin") + input_channel.write(input_data) + input_channel.close() # read stdout and append to output result result: List[str] = [] - for line in iter(process.stdout.readline, ""): + for line in iter(get_io(process, "stdout").readline, ""): line = line.strip() if not line: # skip empty lines continue @@ -95,7 +104,7 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] log(line) # read stderr and write info to logs - for line in iter(process.stderr.readline, ""): + for line in iter(get_io(process, "stderr").readline, ""): log(line.strip()) process.terminate() # make sure that process is terminated @@ -134,6 +143,19 @@ def check_user(paths: RepositoryPaths, unsafe: bool) -> None: raise UnsafeRun(current_uid, root_uid) +def enum_values(enum: Type[Enum]) -> List[str]: + """ + generate list of enumeration values from the source + + Args: + enum(Type[Enum]): source enumeration class + + Returns: + List[str]: available enumeration values as string + """ + return [key.value for key in enum] + + def exception_response_text(exception: requests.exceptions.HTTPError) -> str: """ safe response exception text generation diff --git a/tests/ahriman/application/handlers/test_handler_shell.py b/tests/ahriman/application/handlers/test_handler_shell.py new file mode 100644 index 00000000..03d2b82c --- /dev/null +++ b/tests/ahriman/application/handlers/test_handler_shell.py @@ -0,0 +1,48 @@ +import argparse +import pytest + +from pytest_mock import MockerFixture + +from ahriman.application.handlers import Shell +from ahriman.core.configuration import Configuration + + +def _default_args(args: argparse.Namespace) -> argparse.Namespace: + """ + default arguments for these test cases + + Args: + args(argparse.Namespace): command line arguments fixture + + Returns: + argparse.Namespace: generated arguments for these test cases + """ + args.verbose = False + return args + + +def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run command + """ + args = _default_args(args) + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") + application_mock = mocker.patch("code.interact") + + Shell.run(args, "x86_64", configuration, True, False) + application_mock.assert_called_once_with(local=pytest.helpers.anyvar(int)) + + +def test_run_verbose(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run command with verbose option + """ + args = _default_args(args) + args.verbose = True + mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create") + print_mock = mocker.patch("ahriman.core.formatters.Printer.print") + application_mock = mocker.patch("code.interact") + + Shell.run(args, "x86_64", configuration, True, False) + application_mock.assert_called_once_with(local=pytest.helpers.anyvar(int)) + print_mock.assert_called_once_with(verbose=False) diff --git a/tests/ahriman/application/handlers/test_handler_versions.py b/tests/ahriman/application/handlers/test_handler_versions.py new file mode 100644 index 00000000..d1140a94 --- /dev/null +++ b/tests/ahriman/application/handlers/test_handler_versions.py @@ -0,0 +1,38 @@ +import argparse + +from pytest_mock import MockerFixture +from unittest import mock + +from ahriman.application.handlers import Versions +from ahriman.core.configuration import Configuration + + +def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None: + """ + must run command + """ + application_mock = mocker.patch("ahriman.application.handlers.Versions.package_dependencies") + print_mock = mocker.patch("ahriman.core.formatters.Printer.print") + + Versions.run(args, "x86_64", configuration, True, False) + application_mock.assert_called_once_with("ahriman", ("pacman", "s3", "web")) + print_mock.assert_has_calls([mock.call(verbose=False, separator=" "), mock.call(verbose=False, separator=" ")]) + + +def test_package_dependencies() -> None: + """ + must extract package dependencies + """ + packages = Versions.package_dependencies("srcinfo") + assert packages + assert packages.get("parse") is not None + + +def test_package_dependencies_missing() -> None: + """ + must extract package dependencies even if some of them are missing + """ + packages = Versions.package_dependencies("ahriman", ("docs", "pacman", "s3", "web")) + assert packages + assert packages.get("pyalpm") is not None + assert packages.get("Sphinx") is None diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py index 3d4a9826..8d04c14e 100644 --- a/tests/ahriman/application/test_ahriman.py +++ b/tests/ahriman/application/test_ahriman.py @@ -492,6 +492,15 @@ def test_subparsers_repo_update_architecture(parser: argparse.ArgumentParser) -> assert args.architecture == ["x86_64"] +def test_subparsers_shell(parser: argparse.ArgumentParser) -> None: + """ + shell command must imply lock and no-report + """ + args = parser.parse_args(["shell"]) + assert args.lock is None + assert args.no_report + + def test_subparsers_user_add(parser: argparse.ArgumentParser) -> None: """ user-add command must imply action, architecture, lock, no-report, quiet and unsafe @@ -575,6 +584,26 @@ def test_subparsers_user_remove_architecture(parser: argparse.ArgumentParser) -> assert args.architecture == [""] +def test_subparsers_version(parser: argparse.ArgumentParser) -> None: + """ + version command must imply architecture, lock, no-report, quiet and unsafe + """ + args = parser.parse_args(["version"]) + assert args.architecture == [""] + assert args.lock is None + assert args.no_report + assert args.quiet + assert args.unsafe + + +def test_subparsers_version_architecture(parser: argparse.ArgumentParser) -> None: + """ + version command must correctly parse architecture list + """ + args = parser.parse_args(["-a", "x86_64", "version"]) + assert args.architecture == [""] + + def test_subparsers_web(parser: argparse.ArgumentParser) -> None: """ web command must imply lock, no_report and parser diff --git a/tests/ahriman/core/formatters/conftest.py b/tests/ahriman/core/formatters/conftest.py index d362e40d..7246d195 100644 --- a/tests/ahriman/core/formatters/conftest.py +++ b/tests/ahriman/core/formatters/conftest.py @@ -1,6 +1,7 @@ import pytest -from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, StatusPrinter, StringPrinter, UpdatePrinter, UserPrinter +from ahriman.core.formatters import AurPrinter, ConfigurationPrinter, PackagePrinter, StatusPrinter, StringPrinter, \ + UpdatePrinter, UserPrinter, VersionPrinter from ahriman.models.aur_package import AURPackage from ahriman.models.build_status import BuildStatus from ahriman.models.package import Package @@ -94,3 +95,17 @@ def user_printer(user: User) -> UserPrinter: UserPrinter: user printer test instance """ return UserPrinter(user) + + +@pytest.fixture +def version_printer(package_ahriman: Package) -> VersionPrinter: + """ + fixture for version printer + + Args: + package_ahriman(Package): package fixture + + Returns: + VersionPrinter: version printer test instance + """ + return VersionPrinter("package", {package_ahriman.base: package_ahriman.version}) diff --git a/tests/ahriman/core/formatters/test_version_printer.py b/tests/ahriman/core/formatters/test_version_printer.py new file mode 100644 index 00000000..4483eb5e --- /dev/null +++ b/tests/ahriman/core/formatters/test_version_printer.py @@ -0,0 +1,15 @@ +from ahriman.core.formatters import VersionPrinter + + +def test_properties(version_printer: VersionPrinter) -> None: + """ + must return empty properties list + """ + assert version_printer.properties() + + +def test_title(version_printer: VersionPrinter) -> None: + """ + must return non empty title + """ + assert version_printer.title() is not None diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py index bd125228..1a9c4ddf 100644 --- a/tests/ahriman/core/test_util.py +++ b/tests/ahriman/core/test_util.py @@ -10,8 +10,9 @@ from unittest.mock import MagicMock from ahriman.core.exceptions import BuildFailed, InvalidOption, UnsafeRun from ahriman.core.util import check_output, check_user, exception_response_text, filter_json, full_version, \ - package_like, pretty_datetime, pretty_size, tmpdir, walk + enum_values, package_like, pretty_datetime, pretty_size, tmpdir, walk from ahriman.models.package import Package +from ahriman.models.package_source import PackageSource from ahriman.models.repository_paths import RepositoryPaths @@ -177,6 +178,15 @@ def test_filter_json_empty_value(package_ahriman: Package) -> None: assert "base" not in filter_json(probe, probe.keys()) +def test_enum_values() -> None: + """ + must correctly generate choices from enumeration classes + """ + values = enum_values(PackageSource) + for value in values: + assert PackageSource(value).value == value + + def test_full_version() -> None: """ must construct full version @@ -331,6 +341,7 @@ def test_walk(resource_path_root: Path) -> None: resource_path_root / "web" / "templates" / "build-status.jinja2", resource_path_root / "web" / "templates" / "email-index.jinja2", resource_path_root / "web" / "templates" / "repo-index.jinja2", + resource_path_root / "web" / "templates" / "shell", resource_path_root / "web" / "templates" / "telegram-index.jinja2", ]) local_files = list(sorted(walk(resource_path_root)))