Compare commits

...

2 Commits

Author SHA1 Message Date
a07b20bf50 Release 2.17.1 2025-01-06 01:14:28 +02:00
ed70897c39 fix: suppress traceback in shell if no ipython installed
Old implementation was showing import error, new implementation instead
hides it behind separated call and if-else check
2025-01-06 01:07:13 +02:00
5 changed files with 39 additions and 9 deletions

View File

@ -2,7 +2,7 @@
pkgbase='ahriman' pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web') pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.17.0 pkgver=2.17.1
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2024\-12\-29" "ahriman" "Generated Python Manual" .TH AHRIMAN "1" "2025\-01\-05" "ahriman" "Generated Python Manual"
.SH NAME .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
__version__ = "2.17.0" __version__ = "2.17.1"

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from code import InteractiveConsole from code import InteractiveConsole
from importlib.util import find_spec
from typing import Any from typing import Any
@ -26,6 +27,19 @@ class InteractiveShell(InteractiveConsole):
wrapper around :class:`code.InteractiveConsole` to pass :func:`interact()` to IPython shell wrapper around :class:`code.InteractiveConsole` to pass :func:`interact()` to IPython shell
""" """
@staticmethod
def has_ipython() -> bool:
"""
check if IPython shell is available
Returns:
bool: ``True`` if IPython shell is available, ``False`` otherwise
"""
try:
return find_spec("IPython.terminal.embed") is not None
except ModuleNotFoundError:
return False
def interact(self, *args: Any, **kwargs: Any) -> None: def interact(self, *args: Any, **kwargs: Any) -> None:
""" """
pass controller to IPython shell pass controller to IPython shell
@ -34,13 +48,13 @@ class InteractiveShell(InteractiveConsole):
*args(Any): positional arguments *args(Any): positional arguments
**kwargs(Any): keyword arguments **kwargs(Any): keyword arguments
""" """
try: if self.has_ipython():
from IPython.terminal.embed import InteractiveShellEmbed from IPython.terminal.embed import InteractiveShellEmbed
shell = InteractiveShellEmbed(user_ns=self.locals) # type: ignore[no-untyped-call] shell = InteractiveShellEmbed(user_ns=self.locals) # type: ignore[no-untyped-call]
shell.show_banner() # type: ignore[no-untyped-call] shell.show_banner() # type: ignore[no-untyped-call]
shell.interact() # type: ignore[no-untyped-call] shell.interact() # type: ignore[no-untyped-call]
except ImportError: else:
# fallback to default # fallback to default
import readline # pylint: disable=unused-import import readline # pylint: disable=unused-import
InteractiveConsole.interact(self, *args, **kwargs) InteractiveConsole.interact(self, *args, **kwargs)

View File

@ -1,14 +1,30 @@
import pytest
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.application.interactive_shell import InteractiveShell from ahriman.application.interactive_shell import InteractiveShell
def test_has_ipython(mocker: MockerFixture) -> None:
"""
must correctly check if IPython is installed
"""
find_spec_mock = mocker.patch("ahriman.application.interactive_shell.find_spec")
assert InteractiveShell.has_ipython()
find_spec_mock.assert_called_once_with("IPython.terminal.embed")
def test_has_ipython_module_not_found(mocker: MockerFixture) -> None:
"""
must return False if IPython is not installed
"""
mocker.patch("ahriman.application.interactive_shell.find_spec", side_effect=ModuleNotFoundError)
assert not InteractiveShell.has_ipython()
def test_interact(mocker: MockerFixture) -> None: def test_interact(mocker: MockerFixture) -> None:
""" """
must call IPython shell must call IPython shell
""" """
mocker.patch("ahriman.application.interactive_shell.InteractiveShell.has_ipython", return_value=True)
banner_mock = mocker.patch("IPython.terminal.embed.InteractiveShellEmbed.show_banner") banner_mock = mocker.patch("IPython.terminal.embed.InteractiveShellEmbed.show_banner")
interact_mock = mocker.patch("IPython.terminal.embed.InteractiveShellEmbed.interact") interact_mock = mocker.patch("IPython.terminal.embed.InteractiveShellEmbed.interact")
@ -18,11 +34,11 @@ def test_interact(mocker: MockerFixture) -> None:
interact_mock.assert_called_once_with() interact_mock.assert_called_once_with()
def test_interact_import_error(mocker: MockerFixture) -> None: def test_interact_no_ipython(mocker: MockerFixture) -> None:
""" """
must call builtin shell if no IPython available must call builtin shell if no IPython available
""" """
pytest.helpers.import_error("IPython.terminal.embed", ["InteractiveShellEmbed"], mocker) mocker.patch("ahriman.application.interactive_shell.InteractiveShell.has_ipython", return_value=None)
interact_mock = mocker.patch("code.InteractiveConsole.interact") interact_mock = mocker.patch("code.InteractiveConsole.interact")
shell = InteractiveShell() shell = InteractiveShell()