diff --git a/.github/workflows/setup.sh b/.github/workflows/setup.sh
index 1d16d87f..2a52bfc2 100755
--- a/.github/workflows/setup.sh
+++ b/.github/workflows/setup.sh
@@ -18,7 +18,7 @@ if [[ -z $MINIMAL_INSTALL ]]; then
# 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
# 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
# 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"
diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD
index 1b4922b3..446bd95a 100644
--- a/package/archlinux/PKGBUILD
+++ b/package/archlinux/PKGBUILD
@@ -30,6 +30,7 @@ package_ahriman-core() {
pkgname='ahriman-core'
optdepends=('ahriman-triggers: additional extensions for the application'
'ahriman-web: web server'
+ 'ipython: an enhanced shell interpreter'
'python-boto3: sync to s3'
'python-cerberus: configuration validator'
'python-matplotlib: usage statistics chart'
diff --git a/pyproject.toml b/pyproject.toml
index d878626d..99a0b8c3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -60,6 +60,9 @@ pacman = [
s3 = [
"boto3",
]
+shell = [
+ "IPython"
+]
stats = [
"matplotlib",
]
diff --git a/src/ahriman/application/handlers/shell.py b/src/ahriman/application/handlers/shell.py
index 750f3a09..ca59beb9 100644
--- a/src/ahriman/application/handlers/shell.py
+++ b/src/ahriman/application/handlers/shell.py
@@ -18,12 +18,12 @@
# along with this program. If not, see .
#
import argparse
-import code
import sys
from pathlib import Path
from ahriman.application.handlers.handler import Handler, SubParserAction
+from ahriman.application.interactive_shell import InteractiveShell
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import StringPrinter
from ahriman.models.repository_id import RepositoryId
@@ -58,11 +58,13 @@ class Shell(Handler):
"configuration": configuration,
"repository_id": repository_id,
}
+ console = InteractiveShell(locals=local_variables)
- if args.code is None:
- code.interact(local=local_variables)
- else:
- code.InteractiveConsole(locals=local_variables).runcode(args.code)
+ match args.code:
+ case None:
+ console.interact()
+ case snippet:
+ console.runcode(snippet)
@staticmethod
def _set_service_shell_parser(root: SubParserAction) -> argparse.ArgumentParser:
@@ -79,6 +81,7 @@ class Shell(Handler):
description="drop into python shell")
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("-o", "--output", help="output commands and result to the file", type=Path)
parser.set_defaults(lock=None, report=False)
return parser
diff --git a/src/ahriman/application/interactive_shell.py b/src/ahriman/application/interactive_shell.py
new file mode 100644
index 00000000..ade42175
--- /dev/null
+++ b/src/ahriman/application/interactive_shell.py
@@ -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 .
+#
+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)
diff --git a/tox.ini b/tox.ini
index 28c46c3a..73b3e2cf 100644
--- a/tox.ini
+++ b/tox.ini
@@ -3,7 +3,7 @@ envlist = check, tests
isolated_build = True
labels =
release = version, docs, publish
-dependencies = -e .[journald,pacman,s3,stats,validator,web]
+dependencies = -e .[journald,pacman,s3,shell,stats,validator,web]
project_name = ahriman
[mypy]