Files
ipython-demo/tests/test_shell.py
T

182 lines
6.5 KiB
Python

import contextlib
import importlib.util
import io
import os
import subprocess
import sys
import types
import unittest
from pathlib import Path
from unittest.mock import patch
from IPython.core.interactiveshell import InteractiveShell
from IPython.core.history import HistoryOutput
from IPython.utils import capture
PACKAGE_ROOT = Path(__file__).parents[1] / "src" / "ipython_demo"
package = types.ModuleType("ipython_demo")
package.__path__ = [str(PACKAGE_ROOT)]
sys.modules["ipython_demo"] = package
def load_package_module(name: str):
path = PACKAGE_ROOT / f"{name}.py"
spec = importlib.util.spec_from_file_location(f"ipython_demo.{name}", path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
models_module = load_package_module("models")
events_module = load_package_module("events")
shell_module = load_package_module("shell")
class ShellTests(unittest.TestCase):
def test_init_shell_has_no_seed_escape_hatch(self):
import inspect
self.assertNotIn("seed", inspect.signature(shell_module.Shell.init_shell).parameters)
def test_package_import_has_no_experiment_side_effects(self):
env = os.environ.copy()
env["PYTHONPATH"] = str(PACKAGE_ROOT.parent)
completed = subprocess.run(
[sys.executable, "-c", "import ipython_demo; print('imported')"],
capture_output=True,
text=True,
env=env,
check=True,
)
self.assertEqual(completed.stdout, "imported\n")
def test_event_from_history_output_preserves_streams_and_mime(self):
stdout = events_module.event_from_history_output(
HistoryOutput("out_stream", {"stream": ["hello", "\n"]}),
call_id="call-1",
sequence=3,
)
result = events_module.event_from_history_output(
HistoryOutput("execute_result", {"text/plain": "42"}),
call_id="call-1",
sequence=4,
)
self.assertEqual(stdout, models_module.CallEvent(
call_id="call-1", sequence=3, kind="stdout", text="hello\n"
))
self.assertEqual(result.data, {"text/plain": "42"})
def test_run_cell_and_collect_returns_execution_and_events(self):
shell = InteractiveShell()
shell_module.setup_shell(shell, plt=False, pylab=False)
with contextlib.redirect_stdout(io.StringIO()):
execution, events = shell_module.run_cell_and_collect(
shell,
"print('hello'); 42",
call_id="call-1",
)
self.assertIsInstance(execution, models_module.CallResult)
self.assertEqual(execution.result, 42)
self.assertEqual([event.kind for event in events], ["stdout", "execute_result"])
self.assertEqual(events[0].text, "hello\n")
self.assertEqual(events[1].data, {"text/plain": "42"})
def test_run_cell_and_collect_converts_execution_error(self):
shell = InteractiveShell()
shell_module.setup_shell(shell, plt=False, pylab=False)
with contextlib.redirect_stdout(io.StringIO()):
execution, events = shell_module.run_cell_and_collect(
shell,
"raise ValueError('boom')",
call_id="call-2",
)
self.assertIsInstance(execution.error, models_module.CallError)
self.assertEqual(execution.error.ename, "ValueError")
self.assertEqual(execution.error.evalue, "boom")
self.assertEqual(events[-1].kind, "error")
self.assertEqual(events[-1].data["ename"], "ValueError")
self.assertEqual(events[-1].data["evalue"], "boom")
def test_setup_shell_isolates_history_output_storage(self):
first = InteractiveShell()
second = InteractiveShell()
shell_module.setup_shell(first, plt=False, pylab=False)
shell_module.setup_shell(second, plt=False, pylab=False)
self.assertIsNot(first.history_manager.outputs, second.history_manager.outputs)
def test_setup_shell_configures_mime_output_without_enable_gui(self):
shell = InteractiveShell()
def fail_enable_gui(*args, **kwargs):
raise AssertionError("base InteractiveShell must not require enable_gui")
with patch.object(InteractiveShell, "enable_gui", fail_enable_gui):
shell_module.setup_shell(shell, plt=True, pylab=False)
with contextlib.redirect_stdout(io.StringIO()):
result = shell.run_cell("2 + 2", store_history=True)
self.assertEqual(result.result, 4)
records = shell.history_manager.outputs[result.execution_count]
self.assertEqual([record.output_type for record in records], ["execute_result"])
self.assertEqual(records[0].bundle["text/plain"], "4")
def test_setup_shell_keeps_pylab_namespace_setup_without_enable_gui(self):
shell = InteractiveShell()
def fail_enable_gui(*args, **kwargs):
raise AssertionError("pylab setup must not require enable_gui")
with patch.object(InteractiveShell, "enable_gui", fail_enable_gui):
shell_module.setup_shell(shell, plt=True, pylab=True)
self.assertIn("plt", shell.user_ns)
self.assertIn("np", shell.user_ns)
self.assertIs(shell.user_ns["plt"], shell.user_ns_hidden["plt"])
self.assertIs(shell.user_ns["np"], shell.user_ns_hidden["np"])
def test_setup_shell_installs_matplotlib_runner_without_enable_gui(self):
shell = InteractiveShell()
shell_module.setup_shell(shell, plt=True, pylab=False)
self.assertIsNotNone(
shell.magics_manager.registry["ExecutionMagics"].default_runner
)
def test_run_cell_does_not_use_global_capture_output(self):
wrapper = shell_module.Shell("test")
wrapper.init_shell()
def fail_capture_output(*args, **kwargs):
raise AssertionError("Shell.run_cell must use the shell-local output records")
with patch.object(
capture,
"capture_output",
fail_capture_output,
):
with contextlib.redirect_stdout(io.StringIO()):
result, events = wrapper.run_cell(
"print('hello'); 42",
call_id="call-1",
)
self.assertEqual(result.result, 42)
self.assertEqual([event.kind for event in events], ["stdout", "execute_result"])
self.assertEqual(events[0].text, "hello\n")
if __name__ == "__main__":
unittest.main()