mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-04-24 07:17:17 +00:00
feat: use IPython shell if available
This commit is contained in:
parent
56114ecc1e
commit
4880ca4fee
2
.github/workflows/setup.sh
vendored
2
.github/workflows/setup.sh
vendored
@ -18,7 +18,7 @@ if [[ -z $MINIMAL_INSTALL ]]; then
|
|||||||
# web server
|
# web server
|
||||||
pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
|
pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
|
||||||
# additional features
|
# additional features
|
||||||
pacman -S --noconfirm gnupg python-boto3 python-cerberus python-matplotlib rsync
|
pacman -S --noconfirm gnupg ipython python-boto3 python-cerberus python-matplotlib rsync
|
||||||
fi
|
fi
|
||||||
# FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container
|
# FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container
|
||||||
cp "docker/systemd-nspawn.sh" "/usr/local/bin/systemd-nspawn"
|
cp "docker/systemd-nspawn.sh" "/usr/local/bin/systemd-nspawn"
|
||||||
|
@ -29,6 +29,14 @@ ahriman.application.help\_formatter module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
ahriman.application.interactive\_shell module
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
.. automodule:: ahriman.application.interactive_shell
|
||||||
|
:members:
|
||||||
|
:no-undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.application.lock module
|
ahriman.application.lock module
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ package_ahriman-core() {
|
|||||||
pkgname='ahriman-core'
|
pkgname='ahriman-core'
|
||||||
optdepends=('ahriman-triggers: additional extensions for the application'
|
optdepends=('ahriman-triggers: additional extensions for the application'
|
||||||
'ahriman-web: web server'
|
'ahriman-web: web server'
|
||||||
|
'ipython: an enhanced shell interpreter'
|
||||||
'python-boto3: sync to s3'
|
'python-boto3: sync to s3'
|
||||||
'python-cerberus: configuration validator'
|
'python-cerberus: configuration validator'
|
||||||
'python-matplotlib: usage statistics chart'
|
'python-matplotlib: usage statistics chart'
|
||||||
|
@ -60,6 +60,9 @@ pacman = [
|
|||||||
s3 = [
|
s3 = [
|
||||||
"boto3",
|
"boto3",
|
||||||
]
|
]
|
||||||
|
shell = [
|
||||||
|
"IPython"
|
||||||
|
]
|
||||||
stats = [
|
stats = [
|
||||||
"matplotlib",
|
"matplotlib",
|
||||||
]
|
]
|
||||||
|
@ -18,12 +18,12 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import argparse
|
import argparse
|
||||||
import code
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
|
from ahriman.application.interactive_shell import InteractiveShell
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.formatters import StringPrinter
|
from ahriman.core.formatters import StringPrinter
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
@ -58,11 +58,13 @@ class Shell(Handler):
|
|||||||
"configuration": configuration,
|
"configuration": configuration,
|
||||||
"repository_id": repository_id,
|
"repository_id": repository_id,
|
||||||
}
|
}
|
||||||
|
console = InteractiveShell(locals=local_variables)
|
||||||
|
|
||||||
if args.code is None:
|
match args.code:
|
||||||
code.interact(local=local_variables)
|
case None:
|
||||||
else:
|
console.interact()
|
||||||
code.InteractiveConsole(locals=local_variables).runcode(args.code)
|
case snippet:
|
||||||
|
console.runcode(snippet)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _set_service_shell_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
def _set_service_shell_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||||
@ -79,6 +81,7 @@ class Shell(Handler):
|
|||||||
description="drop into python shell")
|
description="drop into python shell")
|
||||||
parser.add_argument("code", help="instead of dropping into shell, just execute the specified code", nargs="?")
|
parser.add_argument("code", help="instead of dropping into shell, just execute the specified code", nargs="?")
|
||||||
parser.add_argument("-v", "--verbose", help=argparse.SUPPRESS, action="store_true")
|
parser.add_argument("-v", "--verbose", help=argparse.SUPPRESS, action="store_true")
|
||||||
|
parser.add_argument("-o", "--output", help="output commands and result to the file", type=Path)
|
||||||
parser.set_defaults(lock=None, report=False)
|
parser.set_defaults(lock=None, report=False)
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
46
src/ahriman/application/interactive_shell.py
Normal file
46
src/ahriman/application/interactive_shell.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2021-2024 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 code import InteractiveConsole
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class InteractiveShell(InteractiveConsole):
|
||||||
|
"""
|
||||||
|
wrapper around :class:`code.InteractiveConsole` to pass :func:`interact()` to IPython shell
|
||||||
|
"""
|
||||||
|
|
||||||
|
def interact(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
"""
|
||||||
|
pass controller to IPython shell
|
||||||
|
|
||||||
|
Args:
|
||||||
|
*args(Any): positional arguments
|
||||||
|
**kwargs(Any): keyword arguments
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from IPython.terminal.embed import InteractiveShellEmbed
|
||||||
|
|
||||||
|
shell = InteractiveShellEmbed(user_ns=self.locals) # type: ignore[no-untyped-call]
|
||||||
|
shell.show_banner() # type: ignore[no-untyped-call]
|
||||||
|
shell.interact() # type: ignore[no-untyped-call]
|
||||||
|
except ImportError:
|
||||||
|
# fallback to default
|
||||||
|
import readline # pylint: disable=unused-import
|
||||||
|
InteractiveConsole.interact(self, *args, **kwargs)
|
@ -30,11 +30,11 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
|
|||||||
"""
|
"""
|
||||||
args = _default_args(args)
|
args = _default_args(args)
|
||||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
application_mock = mocker.patch("code.interact")
|
application_mock = mocker.patch("ahriman.application.interactive_shell.InteractiveShell.interact")
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
_, repository_id = configuration.check_loaded()
|
||||||
Shell.run(args, repository_id, configuration, report=False)
|
Shell.run(args, repository_id, configuration, report=False)
|
||||||
application_mock.assert_called_once_with(local=pytest.helpers.anyvar(int))
|
application_mock.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
def test_run_eval(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
def test_run_eval(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||||
@ -45,7 +45,7 @@ def test_run_eval(args: argparse.Namespace, configuration: Configuration, reposi
|
|||||||
args = _default_args(args)
|
args = _default_args(args)
|
||||||
args.code = """print("hello world")"""
|
args.code = """print("hello world")"""
|
||||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
application_mock = mocker.patch("code.InteractiveConsole.runcode")
|
application_mock = mocker.patch("ahriman.application.interactive_shell.InteractiveShell.runcode")
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
_, repository_id = configuration.check_loaded()
|
||||||
Shell.run(args, repository_id, configuration, report=False)
|
Shell.run(args, repository_id, configuration, report=False)
|
||||||
@ -62,11 +62,11 @@ def test_run_verbose(args: argparse.Namespace, configuration: Configuration, rep
|
|||||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
read_mock = mocker.patch("pathlib.Path.read_text", return_value="")
|
read_mock = mocker.patch("pathlib.Path.read_text", return_value="")
|
||||||
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
|
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
|
||||||
application_mock = mocker.patch("code.interact")
|
application_mock = mocker.patch("ahriman.application.interactive_shell.InteractiveShell.interact")
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
_, repository_id = configuration.check_loaded()
|
||||||
Shell.run(args, repository_id, configuration, report=False)
|
Shell.run(args, repository_id, configuration, report=False)
|
||||||
application_mock.assert_called_once_with(local=pytest.helpers.anyvar(int))
|
application_mock.assert_called_once_with()
|
||||||
read_mock.assert_called_once_with(encoding="utf8")
|
read_mock.assert_called_once_with(encoding="utf8")
|
||||||
print_mock.assert_called_once_with(verbose=False, log_fn=pytest.helpers.anyvar(int), separator=": ")
|
print_mock.assert_called_once_with(verbose=False, log_fn=pytest.helpers.anyvar(int), separator=": ")
|
||||||
|
|
||||||
|
30
tests/ahriman/application/test_interactive_shell.py
Normal file
30
tests/ahriman/application/test_interactive_shell.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
|
from ahriman.application.interactive_shell import InteractiveShell
|
||||||
|
|
||||||
|
|
||||||
|
def test_interact(mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must call IPython shell
|
||||||
|
"""
|
||||||
|
banner_mock = mocker.patch("IPython.terminal.embed.InteractiveShellEmbed.show_banner")
|
||||||
|
interact_mock = mocker.patch("IPython.terminal.embed.InteractiveShellEmbed.interact")
|
||||||
|
|
||||||
|
shell = InteractiveShell()
|
||||||
|
shell.interact()
|
||||||
|
banner_mock.assert_called_once_with()
|
||||||
|
interact_mock.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
|
def test_interact_import_error(mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must call builtin shell if no IPython available
|
||||||
|
"""
|
||||||
|
pytest.helpers.import_error("IPython.terminal.embed", ["InteractiveShellEmbed"], mocker)
|
||||||
|
interact_mock = mocker.patch("code.InteractiveConsole.interact")
|
||||||
|
|
||||||
|
shell = InteractiveShell()
|
||||||
|
shell.interact()
|
||||||
|
interact_mock.assert_called_once_with(shell)
|
2
tox.ini
2
tox.ini
@ -3,7 +3,7 @@ envlist = check, tests
|
|||||||
isolated_build = True
|
isolated_build = True
|
||||||
labels =
|
labels =
|
||||||
release = version, docs, publish
|
release = version, docs, publish
|
||||||
dependencies = -e .[journald,pacman,s3,stats,validator,web]
|
dependencies = -e .[journald,pacman,s3,shell,stats,validator,web]
|
||||||
project_name = ahriman
|
project_name = ahriman
|
||||||
|
|
||||||
[mypy]
|
[mypy]
|
||||||
|
Loading…
Reference in New Issue
Block a user